Како написати провериви код | Кхалилова методологија

Разумевање писања тестираног кода једна је од највећих фрустрација које сам имао кад сам завршио школу и почео да радим на свом првом стварном послу.

Данас, док сам радио на поглављу у солидбоок.ио, разбијао сам неки код и растављао све погрешно у њему. И схватио сам да неколико принципа управља начином на који пишем код да би се тестирао.

У овом чланку желим да вам представим директну методологију коју можете применити и на предњи и на задњи код како бисте написали код који се може тестирати.

Предуслови читања

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

  • Објашњење убризгавања и инверзије зависности | Ноде.јс в / ТипеСцрипт
  • Правило зависности
  • Принцип стабилне зависности - СДП

Зависности су односи

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

У чланку о инверзији и убризгавању зависности погледали смо пример UserControllerкоме је потребан приступ а 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и за вожњу. Ствар 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који носи дб везу са собом где год стигне.

Ако желимо да тестирамо контролер, можемо задовољити UserController'с потреба за IUserRepoзаменом наш ДБ-бацкед UserRepoза имплементацију у меморији . Можемо створити један попут овог:

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

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

Ево мог размишљања о одржавању кода тестираним. Све почиње када желите да створите везу од једне класе до друге.

Почетак: Желите да увезете или споменете име класе из друге датотеке.

Питање: да ли вам је стало да у будућности будете могли да пишете тестове према изворној класи ?

Ако не , само увезите шта год да је, јер то није важно.

Ако је одговор да , узмите у обзир следећа ограничења. Можете зависити од класе само ако је бар једна од ових:

  • Зависност је апстракција (интерфејс или апстрактна класа).
  • Зависност потиче из истог слоја или из унутрашњег слоја (погледајте правило зависности).
  • То је стабилна зависност.

Ако прође бар један од ових услова, увезите зависност - у супротном немојте.

Увоз зависности уводи могућност да ће у будућности бити тешко тестирати изворну компоненту.

Опет, можете да поправите сценарије у којима зависност крши једно од тих правила помоћу Инверзије зависности.

Пример фронт-енд-а (Реацт в / ТипеСцрипт)

Шта је са фронт-енд развојем?

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

Узмите ову Реацт компоненту (пред-куке) која укључује компоненту контејнера (унутрашњи слој) која зависи од 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је нешто што упућује мрежне позиве на РЕСТфул АПИ, не постоји начин да га тестирамо ProfileContainerи спречимо да упућује стварне АПИ позиве.

То можемо поправити радећи две ствари:

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

Коначно, можемо да саставимо ProfileContainerбило који ХОЦ који желимо - онај који садржи праву услугу или онај који садржи лажну услугу за тестирање.

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

Ја сам Кхалил. Ја сам адвокат за програмере @ Аполло ГрапхКЛ. Такође креирам курсеве, књиге и чланке за амбициозне програмере на Ентерприсе Ноде.јс-у, дизајну вођеном доменом и пишем тестирани, флексибилни ЈаваСцрипт.

Ово је првобитно објављено на мом блогу @ кхалилстеммлер.цом и појављује се у 11. поглављу солидбоок.ио - Увод у дизајн и архитектуру софтвера са Ноде.јс и ТипеСцрипт.

Можете ме контактирати и питати било шта на Твиттеру!