Kako napisati provjerljivi kod | Khalilova metodologija

Razumijevanje pisanja provjerljivog koda jedna je od najvećih frustracija koje sam imao kad sam završio školu i počeo raditi na svom prvom stvarnom poslu.

Danas, dok sam radio na poglavlju u solidbook.io, razbijao sam neki kôd i rastavljao sve pogrešno s njim. Shvatio sam da nekoliko principa upravlja načinom na koji pišem kôd da bi se testirao.

U ovom članku želim vam predstaviti izravnu metodologiju koju možete primijeniti na prednji i stražnji kôd kako napisati provjerljivi kôd.

Preduvjeti za čitanje

Možda ćete prethodno htjeti pročitati sljedeće dijelove. ?

  • Objašnjenje ubrizgavanja i inverzije ovisnosti | Node.js w / TypeScript
  • Pravilo ovisnosti
  • Načelo stabilne ovisnosti - SDP

Ovisnosti su odnosi

To možda već znate, ali prvo što morate shvatiti jest da kada iz jedne klase uvozimo ili čak spomenimo ime druge klase, funkcije ili varijable (nazovimo to izvornom klasom ), sve što je spomenuto postaje ovisnost o izvorna klasa.

U članku o inverziji i ubrizgavanju ovisnosti pogledali smo primjer UserControllerkoji treba pristup a da UserRepobi dobio sve korisnike .

// 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 }); } } 

Problem s ovim pristupom bio je u tome što kada to napravimo, stvaramo tvrdu ovisnost o izvornom kodu .

Veza izgleda ovako:

UserController oslanja se izravno na UserRepo.

To znači da bismo, ako bismo ikada htjeli testirati UserController, morali ponijeti i sebe sa sobom UserRepona vožnju. Stvar UserRepoje u tome što, također, donosi i cijelu vražju vezu s bazom podataka. A to nije dobro.

Ako trebamo zaokretati bazu podataka za pokretanje jediničnih testova, to sve naše jedinične testove usporava.

U konačnici, to možemo popraviti pomoću inverzije ovisnosti , stavljajući apstrakciju između dviju ovisnosti.

Apstrakcije koje mogu invertirati tok ovisnosti jesu ili sučelja ili apstraktne klase .

Korištenje sučelja za implementaciju inverzije ovisnosti.

To funkcionira postavljanjem apstrakcije (sučelja ili apstraktne klase) između ovisnosti koju želite uvesti i izvorne klase. Izvorna klasa uvozi apstrakciju i ostaje provjerljiva jer možemo proslijediti sve što se pridržavalo ugovora o apstrakciji, čak i ako je to lažni objekt .

// 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 }); } } 

U našem scenariju s UserController, sada se odnosi na IUserReposučelje (koje ne košta ništa), a ne na potencijalno teško UserRepokoje nosi db vezu sa sobom gdje god stigne.

Ako želimo testirati kontroler, možemo zadovoljiti UserController„s potrebu za IUserRepozamjenom naš db-podlogom UserRepoza provedbu u memoriji . Možemo stvoriti jedan poput ovog:

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

Metodologija

Evo mog razmišljanja o održavanju koda provjerljivim. Sve započinje kada želite stvoriti odnos iz jedne klase u drugu.

Početak: Želite uvesti ili spomenuti naziv klase iz druge datoteke.

Pitanje: brinete li se o tome da li ćete u budućnosti moći pisati testove prema izvornoj klasi ?

Ako ne , samo naprijed i uvezite što god to bilo, jer to nije važno.

Ako je odgovor da , uzmite u obzir sljedeća ograničenja. O klasi možete ovisiti samo ako je barem jedna od ovih:

  • Ovisnost je apstrakcija (sučelje ili apstraktna klasa).
  • Ovisnost je iz istog sloja ili unutarnjeg sloja (vidi Pravilo ovisnosti).
  • To je stabilna ovisnost.

Ako prođe barem jedan od ovih uvjeta, uvezite ovisnost - u suprotnom nemojte.

Uvoz ovisnosti uvodi mogućnost da će u budućnosti biti teško testirati izvornu komponentu.

Opet, možete popraviti scenarije u kojima ovisnost krši jedno od tih pravila pomoću inverzije ovisnosti.

Primjer prednjeg dijela (Reagiraj s TypeScriptom)

Što je s front-end razvojem?

Primjenjuju se ista pravila!

Uzmite ovu React komponentu (pred-kuke) koja uključuje komponentu spremnika (problem unutarnjeg sloja) koji ovisi o ProfileService(vanjski sloj - 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 ) } } 

Ako ProfileServiceje nešto što upućuje mrežne pozive na RESTful API, ne postoji način da ga testiramo ProfileContaineri spriječimo da upućuje stvarne API pozive.

To možemo popraviti radeći dvije stvari:

1. Stavljanje sučelja između ProfileServiceiProfileContainer

Prvo stvorimo apstrakciju, a zatim osiguravamo da je ProfileServiceona provodi.

// 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 { ... } } 

Sažetak za ProfileService u obliku sučelja.

Zatim se ažuriramo ProfileContainerkako bismo se umjesto toga oslanjali na apstrakciju.

// 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. Sastavite a ProfileContainers HOC-om koji sadrži valjanu IProfileService.

Sada možemo stvoriti HOC-ove koji koriste bilo koju vrstu IProfileServiceželje. To bi mogao biti onaj koji se povezuje s API-jem, kao što slijedi:

// 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; 

Ili može biti lažna koja koristi i uslugu profila u memoriji.

// 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; 

Da bismo ProfileContainermogli koristiti IProfileServiceHOC, on mora očekivati ​​da će primiti IProfileServicekao rekvizit, ProfileContainerumjesto da ga dodamo klasi kao atribut.

// 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 } } 

Finally, we can compose our ProfileContainer with whichever HOC we want- the one containing the real service, or the one containing the fake service for testing.

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")); 

I'm Khalil. I'm a Developer Advocate @ Apollo GraphQL. I also create courses, books, and articles for aspiring developers on Enterprise Node.js, Domain-Driven Design and writing testable, flexible JavaScript.

This was originally posted on my blog @ khalilstemmler.com and appears in Chapter 11 of solidbook.io - An Introduction to Software Design & Architecture w/ Node.js & TypeScript.

You can reach out and ask me anything on Twitter!