如何编写可测试的代码 哈利勒的方法论
Understanding how to write testable code is one of the biggest frustrations I had when I finished school and started working at my first real-world job.
当我完成学业并开始从事第一份现实世界的工作时,了解如何编写可测试的代码是我最大的挫败之一。
Today, while I was working on a chapter in solidbook.io, I was breaking down some code and picking apart everything wrong with it. And I realized that several principles govern how I write code to be testable.
今天,当我在solidbook.io上写一章时,我正在分解一些代码并找出所有错误。 我意识到,有几条原则支配着我如何编写可测试的代码。
In this article, I want to present you with a straightforward methodology you can apply to both front-end and back-end code for how to write testable code.
在本文中,我想向您介绍一种简单的方法,您可以将其应用于前端代码和后端代码,以了解如何编写可测试的代码。
前提读物 (Prerequisite readings)
You may want to read the following pieces beforehand. 😇
您可能需要事先阅读以下内容。 😇
Dependency Injection & Inversion Explained | Node.js w/ TypeScript
依赖注入和反转解释 带TypeScript的Node.js
The Dependency Rule
依赖规则
The Stable Dependency Principle - SDP
稳定依赖原则-SDP
依赖关系 (Dependencies are relationships)
You may already know this, but the first thing to understand is that when we import or even mention the name of another class, function, or variable from one class (let's call this the source class), whatever was mentioned becomes a dependency to the source class.
您可能已经知道这一点,但是首先要了解的是,当我们从一个类中导入甚至提到另一个类,函数或变量的名称(我们称其为source类 )时,所提及的内容都将成为源类 。
In the dependency inversion & injection article, we looked at an example of a UserController
that needed access to a UserRepo
to get all users.
在依赖关系反转和注入文章中 ,我们看了一个UserController
的示例,该示例需要访问UserRepo
才能获取所有用户 。
// controllers/userController.tsimport { 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<void> {const users = await this.userRepo.getUsers();return res.status(200).json({ users });}
}
The problem with this approach was that when we do this, we create a hard source-code dependency.
这种方法的问题在于,当我们这样做时,我们会创建一个硬的源代码依赖项 。
The relationship looks like the following:
关系如下所示:
UserController relies directly on UserRepo.
UserController直接依赖UserRepo。
This means that if we ever wanted to test UserController
, we'd need to bring UserRepo
along for the ride as well. The thing about UserRepo
, though, is that it also brings a whole damn database connection with it as well. And that's no good.
这意味着,如果我们想测试UserController
,我们也需要带UserRepo
。 但是,关于UserRepo
的事情是,它还带来了一个完整的数据库连接。 那不好。
If we need to spin up a database to run unit tests, that makes all our unit tests slow.
如果我们需要启动数据库来运行单元测试,那将使我们所有的单元测试变慢。
Ultimately, we can fix this by using dependency inversion, putting an abstraction between the two dependencies.
最终,我们可以通过使用依赖反转来解决此问题,将抽象放在两个依赖之间。
Abstractions that can invert the flow of dependencies are either interfaces or abstract classes.
可反转的依赖关系的流动抽象要么接口或抽象类 。
Using an interface to implement Dependency Inversion.
使用接口来实现依赖倒置。
This works by placing an abstraction (interface or abstract class) in between the dependency you want to import and the source class. The source class imports the abstraction, and remains testable because we can pass in anything that has adhered to the contract of the abstraction, even if it's a mock object.
通过在要导入的依赖项和源类之间放置一个抽象(接口或抽象类)来进行工作。 源类将导入抽象,并保持可测试性,因为我们可以传入遵守抽象协定的任何内容 ,即使它是模拟对象也是如此 。
// controllers/userController.tsimport { 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 hereconstructor (userRepo: IUserRepo) { // and herethis.userRepo = userRepo;}async handleGetUsers (req, res): Promise<void> {const users = await this.userRepo.getUsers();return res.status(200).json({ users });}
}
In our scenario with UserController
, it now refers to an IUserRepo
interface (which costs nothing) rather than referring to the potentially heavy UserRepo
that carries a db connection with it everywhere it goes.
在我们使用UserController
的场景中,它现在指的是IUserRepo
接口(不花一分钱),而不是指可能很重的UserRepo
,它随处都带有数据库连接。
If we wish to test the controller, we can satisfy the UserController
's need for an IUserRepo
by substituting our db-backed UserRepo
for an in-memory implementation. We can create one like this:
如果要测试控制器,则可以通过将数据库支持的UserRepo
为内存中的实现来满足UserController
对IUserRepo
的需求。 我们可以这样创建一个:
class InMemoryMockUserRepo implements IUserRepo { ... // implement methods and properties
}
方法论 (The methodology)
Here's my thought process for keeping code testable. It all starts when you want to create a relationship from one class to another.
这是我保持代码可测试的过程。 当您要创建一个类到另一个类的关系时,一切就开始了。
Start: You want to import or mention the name of a class from another file.
开始:您想从另一个文件导入或提及一个类的名称。
Question: do you care about being able to write tests against the source class in the future?
问题:您是否担心将来能够针对源类编写测试?
If no, go ahead and import whatever it is because it doesn't matter.
如果不是 ,请继续导入,因为这无关紧要。
If yes, consider the following restrictions. You may depend on the class only if it is at least one of these:
如果是 ,请考虑以下限制。 您只能依赖于至少一个以下类:
- The dependency is an abstraction (interface or abstract class).依赖关系是一个抽象(接口或抽象类)。
The dependency is from the same layer or an inner layer (see The Dependency Rule).
依赖关系来自同一层或内部层(请参阅Dependency Rule )。
It is a stable dependency.
这是一个稳定的依赖关系 。
If at least one of these conditions passes, import the dependency- otherwise, don't.
如果这些条件中的至少一个通过,则导入依赖项;否则,不导入。
Importing the dependency introduces the possibility that it will be hard to test the source component in the future.
导入依赖项可能会导致将来很难测试源组件。
Again, you can fix scenarios where the dependency breaks one of those rules by using Dependency Inversion.
同样,您可以使用Dependency Inversion修复依赖项违反其中一项规则的情况。
前端示例(带TypeScript的React) (Front-end example (React w/ TypeScript))
What about front-end development?
前端开发呢?
The same rules apply!
相同的规则适用!
Take this React component (pre-hooks) involving a container component (inner layer concern) that depends on a ProfileService
(outer layer - infra).
以涉及依赖于ProfileService
(外层-下文)的容器组件 (涉及内层)的React组件(预挂钩)为例。
// containers/ProfileContainer.tsximport * as React from 'react'
import { ProfileService } from './services'; // hard source-code dependency
import { IProfileData } from './models' // stable dependencyinterface ProfileContainerProps {}interface ProfileContainerState {profileData: IProfileData | {};
}export class ProfileContainer extends React.Component<ProfileContainerProps, ProfileContainerState
> {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 (<div>Im a profile container</div>)}
}
If ProfileService
is something that makes network calls to a RESTful API, there's no way for us to test ProfileContainer
and prevent it from making real API calls.
如果ProfileService
可以对RESTful API进行网络调用,则我们无法测试ProfileContainer
并阻止它进行真正的API调用。
We can fix this by doing two things:
我们可以通过做两件事来解决此问题:
1.把一个接口在之间ProfileService
和ProfileContainer
(1. Putting an interface in between the ProfileService
and ProfileContainer
)
First, we create the abstraction and then ensure that ProfileService
implements it.
首先,我们创建抽象,然后确保ProfileService
实现了它。
// services/index.tsx
import { IProfileData } from "../models";// Create an abstraction
export interface IProfileService { getProfile: () => Promise<IProfileData>;
}// Implement the abstraction
export class ProfileService implements IProfileService {async getProfile(): Promise<IProfileData> {...}
}
An abstraction for ProfileService in the form of an interface.
接口形式的ProfileService的抽象。
Then we update ProfileContainer
to rely on the abstraction instead.
然后,我们更新ProfileContainer
以改为依赖抽象。
// 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<ProfileContainerProps, ProfileContainerState
> {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 (<div>Im a profile container</div>)}
}
2.撰写一个ProfileContainer
与包含一个有效的一个HOC IProfileService
。 (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:
现在,我们可以创建使用我们想要的任何IProfileService
。 它可能是连接到API的对象,如下所示:
// hocs/withProfileService.tsximport React from "react";
import { ProfileService } from "../services";interface withProfileServiceProps {}function withProfileService(WrappedComponent: any) {class HOC extends React.Component<withProfileServiceProps, any> {private profileService: ProfileService;constructor(props: withProfileServiceProps) {super(props);this.profileService = new ProfileService();}render() {return (<WrappedComponentprofileService={this.profileService}{...this.props}/>);}}return HOC;
}export default withProfileService;
Or it could be a mock one that uses an in-memory profile service as well.
或者它也可以是模拟的,它也使用内存配置文件服务。
// hocs/withMockProfileService.tsximport * as React from "react";
import { MockProfileService } from "../services";interface withProfileServiceProps {}function withProfileService(WrappedComponent: any) {class HOC extends React.Component<withProfileServiceProps, any> {private profileService: MockProfileService;constructor(props: withProfileServiceProps) {super(props);this.profileService = new MockProfileService();}render() {return (<WrappedComponentprofileService={this.profileService}{...this.props}/>);}}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.
对于我们ProfileContainer
利用的IProfileService
从HOC,它有希望收到IProfileService
作为内道具ProfileContainer
而不是被添加到类为属性。
// containers/ProfileContainer.tsximport * 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<ProfileContainerProps,ProfileContainerState
> {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 <div>Im a profile container</div>}
}
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.
最后,我们可以组成我们ProfileContainer
与取其HOC我们want-一个含有真正的服务,或者包含用于测试的假服务之一。
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<{}, IState> {public render() {return (<div><ProfileContainerWithService /></div>);}
}render(<App />, 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.
我是哈利勒。 我是Apollo GraphQL的开发人员拥护者。 我还为有抱负的开发人员创建了有关Enterprise Node.js,域驱动设计和编写可测试的,灵活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.
它最初发布在我的博客@ khalilstemmler.com上,并出现在solidbook.io的第11章中-带Node.js和TypeScript的软件设计与体系结构简介 。
You can reach out and ask me anything on Twitter!
您可以在Twitter上向我提问!
翻译自: https://www.freecodecamp.org/news/how-to-write-testable-code/
相关文章:

【Java入门提高篇】Day6 Java内部类——成员内部类
内部类是什么,简单来说,就是定义在类内部的类(一本正经的说着废话)。 一个正经的内部类是长这样的: public class Outer {class Inner{} } 这是为了演示而写的类,没有什么luan用,可以看到Inner类…

POJ 1001(高精度乘法 java的2种解法)
方法1: import java.math.BigDecimal; import java.util.Scanner; public class Main {public static void main(String[] args) {Scanner sc new Scanner(System.in);while(sc.hasNext()){String d sc.next();int z sc.nextInt();BigDecimal bd new BigDecimal(d);BigDeci…

Java编写的电梯模拟系统《结对作业》
作业代码:https://coding.net/u/liyi175/p/Dianti/git 伙伴成员:李伊 http://home.cnblogs.com/u/Yililove/ 对于这次作业,我刚开始一点思绪都没有,在老师安排了结对伙伴李伊之后,我的搭档问我,我们需要什么…

HTML属性说明
HTML elements can have attributes, which contain additional information about the element.HTML元素可以具有属性,其中包含有关该元素的其他信息。 HTML attributes generally come in name-value pairs, and always go in the opening tag of an element. Th…

css中的选择器
1.在html中引入css的方法:四种方式: a.行内式(也称内联式) 如: <h1 style"color:red;test</h1> b.内嵌式 <style type"text/css"> h1{ color:red; font-size: 10.5pt; font-family: Calibri, sans-serif; line-height: normal; widow…

javascript的call()方法与apply()方法的理解
先看一段代码 function cat() {} cat.prototype{food:fish,say:function () {console.log(I love this.food);} };var blackCat new cat(); blackCat.say(); 这时,控制台输出 I love fish若此时,有另一个对象 Dog{food:bones and shit}; dog对象没有say…

java排序算法(冒泡,插入,选择,快速,堆,归并,希尔,基数)
import java.util.Arrays; import java.util.LinkedList;/*** * * 各种排序: 冒泡,插入,选择,快速,堆,归并,希尔,基数*/ public class Sorts {//1. 冒泡://时间复杂度:n(n-1)/2O(n^2…

边界填充算法讲解_边界填充算法
边界填充算法讲解Boundary fill is the algorithm used frequently in computer graphics to fill a desired color inside a closed polygon having the same boundary color for all of its sides.边界填充是在计算机图形学中经常使用的算法,用于在其所有边都具有…

使用Git管理源代码
git是个了不起但却复杂的源代码管理系统。它能支持复杂的任务,却因此经常被认为太过复杂而不适用于简单的日常工作。让我们诚实一记吧:Git是复杂的,我们不要装作它不是。但我仍然会试图教会你用(我的)基本的Git和远程代…

[.Net跨平台]部署DTCMS到Jexus遇到的问题及解决思路---Linux环境搭建
最近朋友托我帮忙研究如何把一个DTCMS部署到Linux下,经过1天的研究,部署基本成功,可能有些细节还未注意到,现在把心得分享一下。过程比预期的要简单 身为.Net程序员,这个问题的第一步可能就是如何搭建一个Linux环境来测…

Sequence point 中文
摘自维基百科: In C[4] and C,[5] sequence points occur in the following places. (In C, overloaded operators act like functions, and thus operators that have been overloaded introduce sequence points in the same way as function calls.) Between ev…

python中pop函数_Python中的Pop函数
python中pop函数什么是弹出功能? (What is the pop function?) The method pop() removes and returns the last element from a list. There is an optional parameter which is the index of the element to be removed from the list. If no index is specified…

第六周学习进度条
日期 任务 听课 编程 阅读 准备考试 日总计 周日 周一 120 300 0 0 420 100 周二 0 120 0 0 120 周三 0 0 0 0 0 周四 0 0 0 0 0 周五 0 0 0 0 0 周六 0 120 100 0 …

1071. 小赌怡情(15)
常言道“小赌怡情”。这是一个很简单的小游戏:首先由计算机给出第一个整数;然后玩家下注赌第二个整数将会比第一个数大还是小;玩家下注t个筹码后,计算机给出第二个数。若玩家猜对了,则系统奖励玩家t个筹码;…

关于年长程序员的5个误传
原文链接:http://kb.cnblogs.com/page/150932/ 英文原文:Five Pervasive Myths About Older Software Developers 最近我刚过完40岁生日,一个朋友向我开玩笑地说“嘿,你已经老了,不适合做程序员了!”我虽然…

java中getter_Java中的Getter和Setters解释了
java中getterGetters and setters are used to protect your data, particularly when creating classes. Getter和Setter用于保护数据,尤其是在创建类时。 For each instance variable, a getter method returns its value while a setter method sets or updates…

Loadrunner手动关联详解
Loadrunner手动关联详解 一、关联的含义: 关联(correlation):在脚本回放过程中,客户端发出请求,通过关联函数所定义的左右边界值(也就是关联规则),在服务器所响应的内容中…

解决Visual Studio禁止使用strlen函数的问题
问题描述: 在学习C的复制构造函数以及复制赋值运算符的重载时,需要用到使用C风格的字符串作为引入,由于我用的是VS2015(社区版),在编译时出错。编译器提醒strcpy函数是不安全的,建议改用strlen_…

求整型数组所有子串的和中的最大值
#include <iostream> using namespace std;const int MIN_INT -2147483647;int maxSum(const int *arr, int len){int my_max MIN_INT;int tmp 0;for(int i 0; i < len; i){//从头到尾。。tmp arr[i];//遍历相加。if(my_max < tmp){//更新my_maxmy_max tmp;}…

c语言中的if语句_If ... C中的其他语句解释
c语言中的if语句Conditional code flow is the ability to change the way a piece of code behaves based on certain conditions. In such situations you can use if statements.条件代码流是根据某些条件更改一段代码的行为的能力。 在这种情况下,可以使用if语句…

设计模式之笔记--装饰模式(Decorator)
装饰模式(Decorator) 定义 装饰模式(Decorator),动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。 类图 描述 Component:被装饰者和装饰者共有的基…

整型数组负数放左面,其他放右面,要求时空复杂度:O(n), O(1)。
例如:处理前:{5 -3 6 -7 -6 1 8 -4 0 0},处理后:{-3 -7 -6 -4 5 6 1 8 0 0}. #include <iostream> #include <algorithm> using namespace std;const int LEN 10; void printInt(int c){cout<<c<<"…

[bzoj] 1176 Mokia || CDQ分治
原题 给出WW的矩阵(S没有用,题目有误),给出无限次操作,每次操作的含义为: 输入1:你需要把(x,y)(第x行第y列)的格子权值增加a 输入2:你需要求出以左下角为(x1,y1),右上角为(x2,y2)的矩阵内所有格子的权值和,…

sql子查询示例_SQL更新查询示例说明
sql子查询示例In this article, were going to learn how to use the SQL update statement - what it is, what it can do, and what you need to be aware of before using it.在本文中,我们将学习如何使用SQL更新语句-它是什么,它可以做什么以及在使用…

keepalived+nginx安装
安装keepalivednginx做为公司服务器前端高可用反向代理安装nginx 1、yum install -y pcre pcre-devel gcc-c zlib zlib-devel openssl openssl-devel 2、cd /usr/local/soft 3、wget http://nginx.org/download/nginx-1.12.2.tar.gz 4、tar -zxvf nginx-1.12.2.tar.gz 5、cd ng…

Nexus Repository Manager 3.0 发布
著名仓库管理工具Nexus,在2016年4月6日发布3.0版本(包括OSS版),相较2.*版本有很大的改变: 1. 从底层重构,从而提高性能,增强扩展能力,并改善用户体验 2. 升级界面,增加更…

计算整型数的二进制中包含多少个1
方法很多啊,比如://1.靠循环: int calculate(unsigned int n){int count 0;unsigned int mark 0x1;for(int i 0; i < 32; i){if(n&mark){count;mark<<1;}}return count; }//2. 据说不用循环就能算出来的牛叉方法。木有测试。…

nvm npm不是内部命令_npm作弊表-最常见的命令和nvm
nvm npm不是内部命令npm or the Node Package Manager, is one of the most used tools for any Node.js developer. Heres a list of the most common commands youll use when working with npm.npm或Node Package Manager是Node.js开发人员最常用的工具之一。 这是使用npm时…

快速排序的实现与注意点
先上实现了的C代码: 1 #include <ctime>2 #include <cstdio>3 #include <cstdlib>4 #include <iostream>5 using namespace std;6 const int maxn 100;7 int a[maxn], n;8 void quick_sort(int left, int right) {9 if(left > …

iOS 线程之GCD的高级使用方法
之前的一篇关于线程的blog已经为大家介绍了GCD的简单使用方式及样例说明,今天因为项目中有特殊的应用GCD的实例,为大家介绍两种特殊需求的使用GCD的方法。 目的:实现一件事情做完,再做下一件事情。确保函数的运行周期。 解决方式…