Cómo escribir código comprobable | Metodología de Khalil

Entender cómo escribir código comprobable es una de las mayores frustraciones que tuve cuando terminé la escuela y comencé a trabajar en mi primer trabajo en el mundo real.

Hoy, mientras trabajaba en un capítulo en solidbook.io, estaba rompiendo un código y separando todo lo que estaba mal. Y me di cuenta de que varios principios gobiernan cómo escribo el código para que sea comprobable.

En este artículo, quiero presentarle una metodología sencilla que puede aplicar tanto al código front-end como al back-end sobre cómo escribir código comprobable.

Lecturas de requisitos previos

Es posible que desee leer las siguientes piezas de antemano. ?

  • Explicación de la inyección e inversión de dependencias | Node.js con TypeScript
  • La regla de la dependencia
  • El principio de dependencia estable - SDP

Las dependencias son relaciones

Puede que ya sepa esto, pero lo primero que debe entender es que cuando importamos o incluso mencionamos el nombre de otra clase, función o variable de una clase (llamémosla la clase fuente ), lo que se mencionó se convierte en una dependencia de la clase de fuente.

En el artículo de inversión e inyección de dependencias, vimos un ejemplo de un UserControllerque necesitaba acceso a un UserRepopara obtener todos los usuarios .

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

El problema con este enfoque fue que cuando hacemos esto, creamos una dependencia de código fuente dura .

La relación se parece a la siguiente:

UserController se basa directamente en UserRepo.

Esto significa que si alguna vez quisiéramos hacer una prueba UserController, también tendríamos que llevarlo UserRepopara el viaje. Sin UserRepoembargo, lo que pasa es que también trae una maldita conexión de base de datos. Y eso no es bueno.

Si necesitamos activar una base de datos para ejecutar pruebas unitarias, eso hace que todas nuestras pruebas unitarias sean lentas.

En última instancia, podemos solucionar este problema mediante la inversión de dependencias , poniendo una abstracción entre las dos dependencias.

Las abstracciones que pueden invertir el flujo de dependencias son interfaces o clases abstractas .

Usando una interfaz para implementar la inversión de dependencia.

Esto funciona colocando una abstracción (interfaz o clase abstracta) entre la dependencia que desea importar y la clase fuente. La clase fuente importa la abstracción y permanece comprobable porque podemos pasar cualquier cosa que se haya adherido al contrato de la abstracción, incluso si es un objeto simulado .

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

En nuestro escenario con UserController, ahora se refiere a una IUserRepointerfaz (que no cuesta nada) en lugar de referirse a lo potencialmente pesado UserRepoque lleva consigo una conexión de base de datos donde quiera que vaya.

Si deseamos probar el dispositivo, podemos satisfacer la UserControllernecesidad 's para un IUserReposustituyendo nuestra db respaldados UserRepopor una aplicación en memoria . Podemos crear uno como este:

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

La metodología

Este es mi proceso de pensamiento para mantener el código comprobable. Todo comienza cuando desea crear una relación de una clase a otra.

Inicio: desea importar o mencionar el nombre de una clase desde otro archivo.

Pregunta: ¿le importa poder escribir pruebas contra la clase fuente en el futuro?

Si no , continúe e importe lo que sea porque no importa.

Si , tenga en cuenta las siguientes restricciones. Puede depender de la clase solo si es al menos una de estas:

  • La dependencia es una abstracción (interfaz o clase abstracta).
  • La dependencia proviene de la misma capa o de una capa interna (consulte La regla de dependencia).
  • Es una dependencia estable.

Si pasa al menos una de estas condiciones, importe la dependencia; de lo contrario, no lo haga.

Importar la dependencia introduce la posibilidad de que sea difícil probar el componente fuente en el futuro.

Nuevamente, puede corregir escenarios en los que la dependencia rompe una de esas reglas utilizando Inversión de dependencia.

Ejemplo de interfaz (React w / TypeScript)

¿Qué pasa con el desarrollo de front-end?

¡Se aplican las mismas reglas!

Tome este componente de React (pre-hooks) que involucra un componente contenedor (preocupación de la capa interna) que depende de una ProfileService(capa externa - 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 ) } } 

Si ProfileServicees algo que realiza llamadas de red a una API RESTful, no hay forma de que podamos probar ProfileContainery evitar que realice llamadas API reales.

Podemos solucionar este problema haciendo dos cosas:

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!