Hvordan man skriver testbar kode Khalils metode

At forstå, hvordan man skriver testbar kode, er en af ​​de største frustrationer, jeg havde, da jeg var færdig med skolen og begyndte at arbejde på mit første virkelige job.

I dag, mens jeg arbejdede på et kapitel i solidbook.io, nedbrød jeg noget kode og valgte alt forkert med det. Og jeg indså, at flere principper styrer, hvordan jeg skriver kode for at kunne testes.

I denne artikel vil jeg præsentere dig for en ligetil metode, du kan anvende på både front-end og back-end-kode for, hvordan man skriver testbar kode.

Forudsatte aflæsninger

Det kan være en god idé at læse følgende stykker på forhånd. ?

  • Afhængighedsinjektion og inversion forklaret Node.js m / TypeScript
  • Afhængighedsreglen
  • Det stabile afhængighedsprincip - SDP

Afhængigheder er forhold

Du ved måske allerede dette, men den første ting at forstå er, at når vi importerer eller endda nævner navnet på en anden klasse, funktion eller variabel fra en klasse (lad os kalde dette kildeklassen ), bliver det, der blev nævnt, en afhængighed af kilde klasse.

I artiklen om afhængighedsinversion og injektion kiggede vi på et eksempel på en, UserControllerder havde brug for adgang til en for UserRepoat få alle brugere .

// controllers/userController.ts import { UserRepo } from '../repos' // Bad /** * @class UserController * @desc Responsible for handling API requests for the * /user route. **/ class UserController { private userRepo: UserRepo; constructor () { this.userRepo = new UserRepo(); // Also bad. } async handleGetUsers (req, res): Promise { const users = await this.userRepo.getUsers(); return res.status(200).json({ users }); } } 

Problemet med denne tilgang var, at når vi gør dette, opretter vi en hård kildekodeafhængighed .

Forholdet ser ud som følger:

UserController er afhængig direkte af UserRepo.

Dette betyder, at hvis vi nogensinde ville teste UserController, skulle vi også medbringe UserRepotil turen. Sagen ved UserRepoer dog, at den også bringer en hel forbandet databaseforbindelse med den. Og det er ikke godt.

Hvis vi har brug for at spinde en database op for at køre enhedstest, gør det alle vores enhedstest langsomme.

I sidste ende kan vi løse dette ved at bruge afhængighedsinversion og sætte en abstraktion mellem de to afhængigheder.

Abstraktioner, der kan vende strømmen af ​​afhængigheder, er enten grænseflader eller abstrakte klasser .

Brug af en grænseflade til at implementere afhængighedsinversion.

Dette fungerer ved at placere en abstraktion (interface eller abstrakt klasse) mellem den afhængighed, du vil importere, og kildeklassen. Kildeklassen importerer abstraktionen og forbliver testbar, fordi vi kan videregive alt , hvad der har overholdt abstraktionskontrakten, selvom det er en mock-genstand .

// controllers/userController.ts import { IUserRepo } from '../repos' // Good! Refering to the abstraction. /** * @class UserController * @desc Responsible for handling API requests for the * /user route. **/ class UserController { private userRepo: IUserRepo; // abstraction here constructor (userRepo: IUserRepo) { // and here this.userRepo = userRepo; } async handleGetUsers (req, res): Promise { const users = await this.userRepo.getUsers(); return res.status(200).json({ users }); } } 

I vores scenario med UserControllerhenviser det nu til en IUserRepogrænseflade (som ikke koster noget) snarere end at henvise til det potentielt tunge, UserRepoder bærer en db-forbindelse med det overalt, hvor det går.

Hvis vi ønsker at teste controlleren, kan vi tilfredsstille UserControllerbehovet for et IUserRepoved at erstatte vores db-backede UserRepomed en implementering i hukommelsen . Vi kan oprette en sådan her:

class InMemoryMockUserRepo implements IUserRepo { ... // implement methods and properties } 

Metoden

Her er min tankeproces til at holde kode testbar. Det hele starter, når du vil skabe et forhold fra en klasse til en anden.

Start: Du vil importere eller nævne navnet på en klasse fra en anden fil.

Spørgsmål: er du interesseret i at kunne skrive tests mod kildeklassen i fremtiden?

Hvis nej , fortsæt og importer alt hvad det er, fordi det ikke betyder noget.

Hvis ja , overvej følgende begrænsninger. Du kan kun stole på klassen, hvis den er mindst en af disse:

  • Afhængigheden er en abstraktion (interface eller abstrakt klasse).
  • Afhængigheden er fra det samme lag eller et indre lag (se Afhængighedsreglen).
  • Det er en stabil afhængighed.

If at least one of these conditions passes, import the dependency- otherwise, don't.

Importing the dependency introduces the possibility that it will be hard to test the source component in the future.

Again, you can fix scenarios where the dependency breaks one of those rules by using Dependency Inversion.

Front-end example (React w/ TypeScript)

What about front-end development?

The same rules apply!

Take this React component (pre-hooks) involving a container component (inner layer concern) that depends on a ProfileService (outer layer - infra).

// containers/ProfileContainer.tsx import * as React from 'react' import { ProfileService } from './services'; // hard source-code dependency import { IProfileData } from './models' // stable dependency interface ProfileContainerProps {} interface ProfileContainerState { profileData: IProfileData | {}; } export class ProfileContainer extends React.Component { private profileService: ProfileService; constructor (props: ProfileContainerProps) { super(props); this.state = { profileData: {} } this.profileService = new ProfileService(); // Bad. } async componentDidMount () { try { const profileData: IProfileData = await this.profileService.getProfile(); this.setState({ ...this.state, profileData }) } catch (err) { alert("Ooops") } } render () { return ( Im a profile container ) } } 

If ProfileService is something that makes network calls to a RESTful API, there's no way for us to test ProfileContainer and prevent it from making real API calls.

We can fix this by doing two things:

1. At sætte en grænseflade imellem ProfileServiceogProfileContainer

Først opretter vi abstraktionen og sikrer derefter, at den ProfileServiceimplementeres.

// services/index.tsx import { IProfileData } from "../models"; // Create an abstraction export interface IProfileService { getProfile: () => Promise; } // Implement the abstraction export class ProfileService implements IProfileService { async getProfile(): Promise { ... } } 

En abstraktion for ProfileService i form af en grænseflade.

Derefter opdaterer vi ProfileContaineri stedet for at stole på abstraktionen.

// containers/ProfileContainer.tsx import * as React from 'react' import { ProfileService, IProfileService } from './services'; // import interface import { IProfileData } from './models' interface ProfileContainerProps {} interface ProfileContainerState { profileData: IProfileData | {}; } export class ProfileContainer extends React.Component { private profileService: IProfileService; constructor (props: ProfileContainerProps) { super(props); this.state = { profileData: {} } this.profileService = new ProfileService(); // Still bad though } async componentDidMount () { try { const profileData: IProfileData = await this.profileService.getProfile(); this.setState({ ...this.state, profileData }) } catch (err) { alert("Ooops") } } render () { return ( Im a profile container ) } } 

2. Komponer a ProfileContainermed en HOC, der indeholder en gyldig IProfileService.

Nu kan vi oprette HOC'er, der bruger den slags, IProfileServicevi ønsker. Det kan være den, der opretter forbindelse til en API som det følgende:

// hocs/withProfileService.tsx import React from "react"; import { ProfileService } from "../services"; interface withProfileServiceProps {} function withProfileService(WrappedComponent: any) { class HOC extends React.Component { private profileService: ProfileService; constructor(props: withProfileServiceProps) { super(props); this.profileService = new ProfileService(); } render() { return (  ); } } return HOC; } export default withProfileService; 

Eller det kan også være en mock, der også bruger en profiltjeneste i hukommelsen.

// hocs/withMockProfileService.tsx import * as React from "react"; import { MockProfileService } from "../services"; interface withProfileServiceProps {} function withProfileService(WrappedComponent: any) { class HOC extends React.Component { private profileService: MockProfileService; constructor(props: withProfileServiceProps) { super(props); this.profileService = new MockProfileService(); } render() { return (  ); } } return HOC; } export default withProfileService; 

For at vi ProfileContainerkan bruge den IProfileServicefra en HOC, må den forvente at modtage en IProfileServicesom en prop indenfor i ProfileContainerstedet for at blive føjet til klassen som en attribut.

// containers/ProfileContainer.tsx import * as React from "react"; import { IProfileService } from "./services"; import { IProfileData } from "./models"; interface ProfileContainerProps { profileService: IProfileService; } interface ProfileContainerState { profileData: IProfileData | {}; } export class ProfileContainer extends React.Component { constructor(props: ProfileContainerProps) { super(props); this.state = { profileData: {} }; } async componentDidMount() { try { const profileData: IProfileData = await this.props.profileService.getProfile(); this.setState({ ...this.state, profileData }); } catch (err) { alert("Ooops"); } } render() { return Im a profile container } } 

Endelig kan vi komponere vores ProfileContainermed den HOC vi ønsker - den der indeholder den rigtige tjeneste eller den der indeholder den falske tjeneste til test.

import * as React from "react"; import { render } from "react-dom"; import withProfileService from "./hocs/withProfileService"; import withMockProfileService from "./hocs/withMockProfileService"; import { ProfileContainer } from "./containers/profileContainer"; // The real service const ProfileContainerWithService = withProfileService(ProfileContainer); // The mock service const ProfileContainerWithMockService = withMockProfileService(ProfileContainer); class App extends React.Component { public render() { return ( ); } } render(, document.getElementById("root")); 

Jeg er Khalil. Jeg er udvikleradvokat @ Apollo GraphQL. Jeg opretter også kurser, bøger og artikler til håbende udviklere på Enterprise Node.js, Domain-Driven Design og skriver testbar, fleksibel JavaScript.

Dette blev oprindeligt sendt på min blog @ khalilstemmler.com og vises i kapitel 11 i solidbook.io - En introduktion til softwaredesign og arkitektur m / Node.js & TypeScript.

Du kan nå ud og bede mig om noget på Twitter!