Как да напиша проверяем код | Методология на Халил

Разбирането как да пиша проверяем код е едно от най-големите разочарования, които имах, когато завърших училище и започнах да работя на първата си работа в реалния свят.

Днес, докато работех над глава в solidbook.io, разбивах някакъв код и отделях всичко грешно в него. И разбрах, че няколко принципа управляват начина, по който пиша код, за да се тества.

В тази статия искам да ви представя ясна методология, която можете да приложите както към предния, така и към задния код за това как да напишете проверяем код.

Предварителни показания

Може да искате да прочетете следните парчета предварително. ?

  • Обяснение на инжектирането и инверсията на зависимостите | Node.js с TypeScript
  • Правилото за зависимост
  • Принципът на стабилна зависимост - SDP

Зависимостите са взаимоотношения

Може би вече знаете това, но първото нещо, което трябва да разберете е, че когато импортираме или дори споменем името на друг клас, функция или променлива от един клас (нека наречем това изходен клас ), каквото и да е било споменато, става зависимост от източник клас.

В статията за инверсия и инжектиране на зависимости разгледахме пример за a, UserControllerкойто се нуждаеше от достъп до a, за UserRepoда получи всички потребители .

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

Проблемът с този подход беше, че когато правим това, ние създаваме твърда зависимост от изходния код .

Връзката изглежда по следния начин:

UserController разчита директно на UserRepo.

Това означава, че ако някога сме искали да тестваме UserController, ще трябва да вземем UserRepoи за пътуването. Въпросът UserRepoобаче е, че той също носи цяла проклета връзка с базата данни. И това не е добре.

Ако трябва да завъртим база данни, за да стартираме модулни тестове, това прави всичките ни модулни тестове бавни.

В крайна сметка можем да поправим това, като използваме инверсия на зависимостите , поставяйки абстракция между двете зависимости.

Абстракциите, които могат да обърнат потока от зависимости, са или интерфейси, или абстрактни класове .

Използване на интерфейс за реализиране на инверсия на зависимости.

Това работи чрез поставяне на абстракция (интерфейс или абстрактен клас) между зависимостта, която искате да импортирате, и класа на източника. Класът източник импортира абстракцията и остава проверяем, защото можем да предадем всичко, което е спазило договора за абстракцията, дори ако това е фиктивен обект .

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

В нашия сценарий с UserController, той вече се отнася до IUserRepoинтерфейс (който не струва нищо), а не към потенциално тежък, UserRepoкойто носи db връзка с него навсякъде, където отиде.

Ако искаме да тестваме контролера, можем да задоволим UserControllerнуждата от a, IUserRepoкато заменим db-backed UserRepoза изпълнение в паметта . Можем да създадем такъв:

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

Методологията

Ето моят мисловен процес за поддържане на кода за тестване. Всичко започва, когато искате да създадете връзка от един клас с друг.

Старт: Искате да импортирате или споменете името на клас от друг файл.

Въпрос: грижите ли се да можете да пишете тестове срещу класа на източника в бъдеще?

Ако не , продължете и импортирайте каквото и да е, защото няма значение.

Ако отговорът е да , помислете за следните ограничения. Може да разчитате на класа само ако е поне един от следните:

  • Зависимостта е абстракция (интерфейс или абстрактен клас).
  • Зависимостта е от същия слой или вътрешен слой (вж. Правилото за зависимост).
  • Това е стабилна зависимост.

Ако поне едно от тези условия премине, импортирайте зависимостта - в противен случай не го правете.

Импортирането на зависимостта въвежда възможността да бъде трудно да се тества компонентът източник в бъдеще.

Отново можете да коригирате сценарии, при които зависимостта нарушава едно от тези правила, като използвате инверсия на зависимости.

Пример отпред (React w / TypeScript)

Какво ще кажете за разработката отпред?

Важат същите правила!

Вземете този компонент на React (предварителни куки), включващ компонент на контейнер (загриженост за вътрешния слой), който зависи от ProfileService(външен слой - инфра).

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

Ако ProfileServiceе нещо, което прави мрежови повиквания към RESTful API, няма начин да тестваме ProfileContainerи да му попречим да прави реални API повиквания.

Можем да поправим това, като направим две неща:

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!