Hogyan írjunk tesztelhető kódot Khalil módszertana

A tesztelhető kód megírásának megértése az egyik legnagyobb csalódás volt, amikor befejeztem az iskolát és elkezdtem dolgozni az első valós munkahelyemen.

Ma, miközben a solidbook.io egyik fejezetén dolgoztam, lebontottam egy kódot, és szétválasztottam minden hibát. És rájöttem, hogy több elv vezérli, hogy miként írhatok kódot tesztelhetővé.

Ebben a cikkben egy egyszerű módszertant szeretnék bemutatni Önnek, amelyet mind a kezelőfelület, mind a háttérkódra alkalmazhat a tesztelhető kód írásához.

Előfeltételek

Érdemes előtte elolvasnia a következő darabokat. ?

  • Függőségi injekció és inverzió magyarázata | Node.js w / TypeScript
  • A függőségi szabály
  • A stabil függőség elve - SDP

A függőségek kapcsolatok

Lehet, hogy már tudod ezt, de az első dolog, amit meg kell érteni, hogy amikor egy másik osztály, függvény vagy változó nevét importáljuk, vagy akár megemlítjük egy osztályból (nevezzük ezt forrásosztálynak ), akkor bármi, amit említettünk, a forrás osztály.

A függőségi inverzió és injekció cikkben megvizsgáltunk egy példát UserController, amelyhez hozzáférés szükséges a- UserRepohoz, hogy minden felhasználót elérhessen .

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

Ezzel a megközelítéssel az volt a probléma, hogy amikor ezt megtettük, létrehozunk egy kemény forráskód-függőséget .

A kapcsolat a következőképpen néz ki:

A UserController közvetlenül a UserRepo-ra támaszkodik.

Ez azt jelenti, hogy ha valaha is tesztelni UserControllerakarunk, akkor magunkkal kell vinnünk UserRepoa menetet is. A lényeg UserRepoazonban az, hogy ez egy átkozott adatbázis-kapcsolatot is hoz magával. És ez nem jó.

Ha az egységtesztek futtatásához adatbázisra van szükségünk, az összes egységtesztünket lassúvá teszi.

Végül ezt meg tudjuk javítani a függőségi inverzió használatával, absztrakciót téve a két függőség közé.

Az absztrakciók , amelyek megfordíthatják a függőségek áramlását, interfészek vagy absztrakt osztályok .

Felület használata a Dependency Inversion megvalósításához.

Ez úgy működik, hogy absztrakciót (interfészt vagy absztrakt osztályt) helyez el az importálni kívánt függőség és a forrásosztály közé. A forrásosztály importálja az absztrakciót, és tesztelhető marad, mert bármit átadhatunk , ami betartotta az absztrakció szerződését, még akkor is, ha ez egy csúfobjektum .

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

A (z) forgatókönyvünkben UserControllermost egy IUserRepointerfészre utal (amely semmibe sem kerül), ahelyett, hogy a potenciálisan nehézre utalna UserRepo, amely bárhová visz db db kapcsolatot.

Ha szeretnénk, hogy teszteljék a vezérlő, ki tudjuk elégíteni a UserController„s szükség van egy IUserRepohelyettesítjük a db-fedezetű UserRepoegy in-memory végrehajtását . Készíthetünk ilyet:

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

A módszertan

Itt van a gondolkodási folyamatom a kód tesztelhetőségének megőrzésére. Az egész akkor kezdődik, amikor kapcsolatot akarsz létrehozni egyik osztályból a másikba.

Kezdés: Importálni vagy megemlíteni egy osztály nevét egy másik fájlból.

Kérdés: törődik azzal, hogy a jövőben teszteket tudjon írni a forrásosztály ellen ?

Ha nem , folytassa és importálja bármi, mert nem számít.

Ha igen , vegye figyelembe a következő korlátozásokat. Csak akkor függhet az osztálytól, ha az alábbiak közül legalább az egyik :

  • A függőség absztrakció (interfész vagy absztrakt osztály).
  • A függőség ugyanabból vagy egy belső rétegből származik (lásd: A függőségi szabály).
  • Ez egy stabil függőség.

Ha ezen feltételek közül legalább egy teljesül, importálja a függőséget - különben ne.

A függőség importálása felveti annak lehetőségét, hogy a jövőben nehéz lesz tesztelni a forráskomponentust.

Ismét kijavíthatja azokat a forgatókönyveket, amikor a függőség megsérti az egyik ilyen szabályt, a Függőség inverzió használatával.

Front-end példa (React w / TypeScript)

Mi a helyzet a front-end fejlesztéssel?

Ugyanezek a szabályok érvényesek!

Vegyük ezt a React komponenst (előhorgok) egy tartálykomponens bevonásával (belső réteg aggálya), amely egy ProfileService(külső rétegtől - infra) függ .

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

Ha ProfileServicevalami olyasmit tesz, hogy hálózati hívásokat kezdeményez egy RESTful API-hoz, akkor nincs módunk tesztelni ProfileContainerés megakadályozni, hogy valódi API-hívásokat indítsunk.

Ezt két dologgal tudjuk kijavítani:

1. Putting an interface in between the ProfileService and ProfileContainer

First, we create the abstraction and then ensure that ProfileService implements it.

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

An abstraction for ProfileService in the form of an interface.

Then we update ProfileContainer to rely on the abstraction instead.

// 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. Compose a ProfileContainer with a HOC that contains a valid IProfileService.

Now we can create HOCs that use whatever kind of IProfileService we wish. It could be the one that connects to an API like what follows:

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

Or it could be a mock one that uses an in-memory profile service as well.

// 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 our ProfileContainer to utilize the IProfileService from an HOC, it has to expect to receive an IProfileService as a prop within ProfileContainer rather than being added to the class as an attribute.

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