Τρόπος σύνταξης δοκιμαστικού κώδικα | Η μεθοδολογία του Khalil

Η κατανόηση του τρόπου σύνταξης δοκιμαστικού κώδικα είναι μια από τις μεγαλύτερες απογοητεύσεις που είχα όταν τελείωσα το σχολείο και άρχισα να δουλεύω στην πρώτη μου πραγματική δουλειά.

Σήμερα, ενώ δούλευα σε ένα κεφάλαιο στο solidbook.io, έσπαζα κάποιον κώδικα και ξεχώριζα όλα τα λάθη με αυτό. Και συνειδητοποίησα ότι αρκετές αρχές διέπουν τον τρόπο με τον οποίο γράφω κώδικα για να είναι δοκιμή.

Σε αυτό το άρθρο, θέλω να σας παρουσιάσω μια απλή μεθοδολογία που μπορείτε να εφαρμόσετε σε κώδικα front-end και back-end για τον τρόπο σύνταξης δοκιμαστικού κώδικα.

Προαπαιτούμενες αναγνώσεις

Μπορεί να θέλετε να διαβάσετε τα παρακάτω κομμάτια εκ των προτέρων. ;

  • Εξήγηση εξάρτησης & αναστροφή εξήγησης | Node.js w / TypeScript
  • Ο κανόνας της εξάρτησης
  • Η αρχή της σταθερής εξάρτησης - SDP

Οι εξαρτήσεις είναι σχέσεις

Ίσως το γνωρίζετε ήδη, αλλά το πρώτο πράγμα που πρέπει να καταλάβετε είναι ότι όταν εισαγάγουμε ή ακόμα και αναφέρουμε το όνομα μιας άλλης κλάσης, μιας συνάρτησης ή μιας μεταβλητής από μια τάξη (ας το ονομάσουμε το class source ), ό, τι αναφέρθηκε γίνεται εξάρτηση από το τάξη πηγής.

Στο άρθρο αντιστροφής & έγχυσης εξάρτησης, εξετάσαμε ένα παράδειγμα ενός 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.

Αυτό σημαίνει ότι αν θέλαμε ποτέ να δοκιμάσουμε 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ανάγκη για ένα IUserRepo, αντικαθιστώντας το db-backed μας UserRepoγια μια εφαρμογή στη μνήμη . Μπορούμε να δημιουργήσουμε ένα τέτοιο:

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

Η μεθοδολογία

Εδώ είναι η διαδικασία σκέψης μου για τη διατήρηση του κώδικα. Όλα ξεκινούν όταν θέλετε να δημιουργήσετε μια σχέση από τη μία τάξη στην άλλη.

Έναρξη: Θέλετε να εισαγάγετε ή να αναφέρετε το όνομα μιας κλάσης από άλλο αρχείο

Ερώτηση: Σας ενδιαφέρει να είστε σε θέση να γράψετε τεστ έναντι της κατηγορίας πηγής στο μέλλον;

Εάν όχι , προχωρήστε και εισαγάγετε ό, τι είναι επειδή δεν έχει σημασία.

Εάν ναι , λάβετε υπόψη τους ακόλουθους περιορισμούς. Μπορεί να εξαρτάται από την τάξη μόνο εάν είναι τουλάχιστον ένα από αυτά:

  • Η εξάρτηση είναι μια αφαίρεση (διεπαφή ή αφηρημένη τάξη).
  • Η εξάρτηση προέρχεται από το ίδιο στρώμα ή από ένα εσωτερικό στρώμα (βλ. The Dependency Rule).
  • Είναι μια σταθερή εξάρτηση.

Εάν περάσει τουλάχιστον μία από αυτές τις συνθήκες, εισαγάγετε την εξάρτηση - διαφορετικά, μην το κάνετε.

Η εισαγωγή της εξάρτησης εισάγει την πιθανότητα ότι θα είναι δύσκολο να δοκιμαστεί το στοιχείο προέλευσης στο μέλλον.

Και πάλι, μπορείτε να διορθώσετε σενάρια όπου η εξάρτηση σπάει έναν από αυτούς τους κανόνες χρησιμοποιώντας το Dependency Inversion.

Παράδειγμα διεπαφής (React w / TypeScript)

Τι γίνεται με την ανάπτυξη front-end;

Ισχύουν οι ίδιοι κανόνες!

Πάρτε αυτό το συστατικό 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 } } 

Τέλος, μπορούμε να συνθέσουμε ProfileContainerμε όποιο HOC θέλουμε - αυτό που περιέχει την πραγματική υπηρεσία ή αυτό που περιέχει την ψεύτικη υπηρεσία για δοκιμή.

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

Είμαι ο Χαλίλ. Είμαι Advocate προγραμματιστή @ Apollo GraphQL. Δημιουργώ επίσης μαθήματα, βιβλία και άρθρα για επίδοξους προγραμματιστές στο Enterprise Node.js, το Domain-Driven Design και τη συγγραφή δοκιμαστικών, ευέλικτων JavaScript.

Αρχικά δημοσιεύτηκε στο blog μου @ khalilstemmler.com και εμφανίζεται στο Κεφάλαιο 11 του solidbook.io - Εισαγωγή στη σχεδίαση και την αρχιτεκτονική λογισμικού w / Node.js & TypeScript.

Μπορείτε να επικοινωνήσετε μαζί μου και να με ρωτήσετε οτιδήποτε στο Twitter!