Testattavan koodin kirjoittaminen Khalilin metodologia

Testattavan koodin kirjoittamisen ymmärtäminen on yksi suurimmista turhautumisista, joita minulla oli, kun lopetin koulun ja aloin työskennellä ensimmäisessä tosielämässä.

Tänään, kun työskentelin solidbook.io: n luvun kanssa, hajotin koodia ja poimin kaiken väärän. Ja tajusin, että useita periaatteita ohjaa kuinka kirjoitan koodin testattavaksi.

Tässä artikkelissa haluan esitellä sinulle yksinkertaisen menetelmän, jota voit käyttää sekä käyttöliittymän että taustakoodin kanssa testattavan koodin kirjoittamiseen.

Vaadittavat lukemat

Voit halutessasi lukea seuraavat kappaleet etukäteen. ?

  • Riippuvuuden injektio ja inversio selitetty | Node.js w / TypeScript
  • Riippuvuussääntö
  • Vakaa riippuvuusperiaate - SDP

Riippuvuudet ovat suhteita

Saatat jo tietää tämän, mutta ensimmäinen asia, joka on ymmärrettävä, on se, että kun tuomme tai jopa mainitsemme toisen luokan, funktion tai muuttujan nimen yhdestä luokasta (kutsutaan tätä lähdeluokaksi ), kaikesta mainitusta tulee riippuvuus lähdeluokka.

Vuonna riippuvuus käännellen ja injektio artikkeli, me katsoimme esimerkki UserController, joka tarvitsee liittymän UserRepoon saada kaikille käyttäjille .

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

Tämän lähestymistavan ongelmana oli, että kun teemme tämän, luomme kovan lähdekoodiriippuvuuden .

Suhde näyttää seuraavalta:

UserController luottaa suoraan UserRepoon.

Tämä tarkoittaa sitä, että jos haluamme koskaan testata UserController, meidän on UserRepomyös otettava mukaan matka. Asia on UserRepokuitenkin siinä, että se tuo myös koko pirun tietokantayhteyden siihen. Ja se ei ole hyvä.

Jos meidän on kehitettävä tietokanta yksikötestien suorittamiseksi, kaikki yksikötestimme hidastuvat.

Viime kädessä voimme korjata tämän käyttämällä riippuvuusinversiota asettamalla abstraktio kahden riippuvuuden välille.

Abstraktiot, jotka voivat kääntää riippuvuuksien virtauksen, ovat joko rajapintoja tai abstrakteja luokkia .

Käyttöliittymän avulla riippuvuuden kääntäminen.

Tämä toimii asettamalla abstraktio (käyttöliittymä tai abstrakti luokka) tuotavan riippuvuuden ja lähdeluokan väliin. Lähdeluokka tuo abstraktion ja pysyy testattavana, koska voimme viedä sisään kaiken, mikä on kiinni abstraktion sopimuksesta, vaikka se olisi pilkkoobjekti .

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

Skenaariossa, jossa UserControllerse viittaa nyt IUserRepokäyttöliittymään (joka ei maksa mitään) sen sijaan, että viitattaisiin potentiaalisesti raskaaseen UserRepo, jolla on db-yhteys sen kanssa kaikkialla.

Jos haluamme testata ohjaimeen, voimme tyydyttää UserControllern tarve IUserRepokorvaamalla meidän db tukemien UserRepovarten muistissa täytäntöönpanoon . Voimme luoda yhden tällaisen:

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

Menetelmät

Tässä on ajatusprosessini koodin pitämiseksi testattavana. Kaikki alkaa, kun haluat luoda suhteen luokasta toiseen.

Alku: Haluat tuoda tai mainita luokan nimen toisesta tiedostosta.

Kysymys: välitätkö siitä, että voit kirjoittaa testejä lähdeluokkaa vastaan tulevaisuudessa?

Jos ei , siirry eteenpäin ja tuo mikä tahansa, koska sillä ei ole merkitystä.

Jos kyllä , ota huomioon seuraavat rajoitukset. Voit luottaa luokkaan vain, jos se on vähintään yksi näistä:

  • Riippuvuus on abstraktio (rajapinta tai abstrakti luokka).
  • Riippuvuus on samasta kerroksesta tai sisemmästä kerroksesta (katso riippuvuussääntö).
  • Se on vakaa riippuvuus.

Jos ainakin yksi näistä ehdoista täyttyy, tuo riippuvuus - muuten älä.

Riippuvuuden tuominen tuo esiin mahdollisuuden, että lähdekomponenttia on vaikea testata tulevaisuudessa.

Jälleen voit korjata skenaariot, joissa riippuvuus rikkoo yhden näistä säännöistä, käyttämällä riippuvuuden kääntämistä.

Esimerkki käyttöliittymästä (React w / TypeScript)

Entä etupään kehitys?

Samat säännöt ovat voimassa!

Ota tämä React-komponentti (koukut), johon kuuluu säiliökomponentti (sisäkerroksen ongelma), joka riippuu ProfileService(ulkokerroksesta - 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 ) } } 

Jos ProfileServiceon jotain, joka soittaa verkkokutsuja RESTful-sovellusliittymään, emme voi mitenkään testata ProfileContainerja estää sitä tekemästä todellisia API-puheluja.

Voimme korjata tämän tekemällä kaksi asiaa:

1. Rajapinnan asettaminen ProfileServicejaProfileContainer

Ensin luomme abstraktion ja sitten varmistamme, että ProfileServicese toteutetaan.

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

ProfileServicen abstraktio käyttöliittymän muodossa.

Sitten päivitämme ProfileContainersen sijaan, että luotamme abstraktioon.

// 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. Kirjoita a ProfileContainerja HOC, jotka sisältävät kelvollisen IProfileService.

Nyt voimme luoda HOC: ita, jotka käyttävät mitä tahansa haluamiamme IProfileService. Se voi olla se, joka muodostaa yhteyden sovellusliittymään, kuten seuraava:

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

Tai se voi olla pilkka, joka käyttää myös muistin sisäistä profiilipalvelua.

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

Jotta ProfileContainervoisimme käyttää IProfileServiceHOC: sta peräisin olevaa tietoa, sen on odotettava saavansa IProfileServicepotkurin sisällä ProfileContainersen sijaan, että se lisätään luokkaan attribuuttina.

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

Lopuksi voimme säveltää ProfileContainerhaluamasi HOC: n - sen, joka sisältää todellisen palvelun tai sen, joka sisältää väärennetyn palvelun testattavaksi.

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

Olen Khalil. Olen kehittäjien puolestapuhuja @ Apollo GraphQL. Luon myös kursseja, kirjoja ja artikkeleita kehittyneille kehittäjille Enterprise Node.js: ssä, Domain-Driven Designissa ja kirjoitan testattavaa, joustavaa JavaScriptiä.

Tämä julkaistiin alun perin blogissani @ khalilstemmler.com ja se näkyy solidbook.io: n luvussa 11 - Johdatus ohjelmistosuunnitteluun ja arkkitehtuuriin w / Node.js ja TypeScript.

Voit ottaa yhteyttä ja kysyä minulta mitä tahansa Twitterissä!