1. 背景
当你想为自己的代码写单元测试的时候发现自己无从下手,不知道怎么写测试。而出现这样的问题,主要原因就在于前期设计的时候或者写代码的时候根本没有考虑“可测试性”。导致代码的可测试性不够好。这个时候,我们要重构自己的代码,让其更容易测试。
2. 什么是代码的可测试性
我们知道,软件开发要解决的问题是从需求而来。需求包括两大类,
- 第一类是功能性需求,也就是要完成怎样的业务功能;
- 第二类是非功能性需求,是业务功能之外的一些需求。非功能性需求也被分为两大类,
- 一类称为执行质量(Execution qualities),例如:吞吐、延迟、安全就属于这一类,它们都是可以在运行时通过运维手段被观察到的;
- 另一类称为演化质量(Evolution qualities),它们内含于一个软件的结构之中,包括可测试性、可维护性、可扩展性等。
2.1 可测试性为什么如此重要
我们做设计时,其实就是把一个软件拆分成一个一个的小模块。如果不尽可能地保证每个小模块的正确性,而只是从最外围的系统角度去验证系统的正确性,这将会是一个非常困难的过程。就如电视厂家组装电视机,如果在组装前不能保证各个电器元件合格,那么有可能组装后的电视无法正常工作。这就是很多团队面临的尴尬场景:每个模块都没有验证过,只知道系统集成起来能够工作。
代码不好测,其实就是可测试性不好。当我们添加了一个新功能时,如果必须把整个系统启动起来,然后给系统发消息,再到数据库里写 SQL 把查数据去做对比,这是非常麻烦的一件事。为了一个简单的功能兜这么大一圈,这无论如何都是可测试性很糟糕的表现。然而,这却是很多团队测试的真实状况。因为系统每个模块的可测试性不好,所以,最终只能把整个系统都集成起来进行系统测试。
相比于单元测试集成测试是一种非常耗时的。还有集成测试验证的是本服务代码和其他进程的服务能不能一起配合工作。而业务逻辑。领域模块的业务逻辑应该放在单元测试中完成。
相应地,对一个可测试性好的系统而言,应该每个模块都可以进行独立的测试。在我们把每一个组件都测试稳定之后,再把这些组件组装起来进行验证,这样逐步构建起来的系统,我对它的质量是放心的。即便是要改动某些部分,有了相应的测试做保证,才敢于放手去改。
3. 编写可测试的代码
3.1 被测试类中对其他类的依赖
对被测试类中对其他类的依赖的解决方法可以分为两种
- 第一种孤立型,我只关注我的测试目标Class,而Dependency Class一律用Mock来替代,Mock只是模仿简单的交互。
- 另一种是社交型,我还是关注我的测试目标Class,但是Depdency Class用真实的、已经实现好的Class。
孤立型和社交型方法的比较
- 独立型的好处是确实独立,不受依赖影响,而且速度快,但是需要花费成本来开发Mock Class。
- 而社交型的好处是没有任何开发成本,但是有一个测试顺序的路径依赖,先测试依赖少的Class,最后才能测试依赖最多的那个Class。
- 社交型的单元测试:受测对象会直接使用真实的依赖类别,让测试案例真实地运行一个完整的行为。
- 孤立型的单元测试:受测对象将不会使用真实的依赖对象。因为依赖对象发生错误,也会造成单元测试无法通过!为了确保受测程序不被影响,孤立型单元测试 会利用测试替身(Test Doubles)仿真并隔离依赖
另外,上述提到的测试替身是一种在测试中使用对象代替实际对象的技术,常用的技术如下。
-
桩代码(Stubs):当在对象上调用特定方法时,会对其进行硬编码(临时代码)的方式来代替真实代码提供固定响应。比如,某函数 X 的实现中调用了一个函数 Y,而 Y 不能调用,为了对函数 X 进行测试,就需要模拟一个函数 Y,那么函数 Y 的实现就是所谓的桩代码。
-
模拟代码(Mocks):模拟代码跟桩代码类似,它除了代替真实代码的能力之外,更强调是否使用了特定的参数调用了特定方法,因此,这种对象成为我们测试结果的基础。
3.2 ## 社交型的单元测试与孤立型的单元测试怎么选择
首先要看外部依赖的特征,我把它划分成2种类型。
-
完全可控依赖
-
不可控依赖
3.2.1 完全可控依赖
这个外部的服务被你的应用独享,你也能够控制它的开发和运维,那这个服务就是完全可控依赖的。一个典型的例子,就是数据库,在微服务模式下,每一个服务独享一个自己的数据库Schema。
3.2.2 不可控依赖
与可控依赖相反,这个外部的服务不止你的应用调用,大家都得遵守一个协议或规范,和这个公共的外部服务交互。典型的例子,就是外部的支付系统,SMTP邮件通知服务等等。
| 可控依赖 | 不可控依赖 | |
|---|---|---|
| 孤立型 | 不推荐 | 推荐 |
| 社交型 | 推荐 | 不推荐 |
为什么是这样的? 完全可控依赖的服务,虽然是在你的应用之外的一个进程,但你可以把跟它的交互当作是你开发的内部实现。你可以升级数据库版本、修改表格结构、增加数据库函数,只要跟着应用的代码一起修改即可。
这种情况下,你可以把这个数据库和你的应用当作一个整体,没必要花力气做Mock,如果做了Mock,就还要维护Mock的变化。
而不可控依赖服务就不一样了,它是公共的,你控制不了它,而且你跟它的交互还要遵守一个规范的契约。在这种情况下,做Mock就划算了,原因有二:
- 第一,基于契约的Mock的维护成本比较低;
- 第二,使用Mock可以保证你的应用持续重构,向后兼容。
当然也有一些不得不使用孤立型单元测试的场景,比如一些比较难构造的Object:这类Object通常有很多依赖,在单元测试中构造出这样类通常花费的成本太大。
4. 一个demo
单元测试容易写、覆盖率高、干净易懂,这又叫代码可测试性。
假如订餐系统有一个需求,希望用户在页面上可以修改自己的邮箱地址。
这个修改行为后端实现起来似乎很简单,修改邮箱就是更新数据库里的用户信息。但在FoodCome平台上有一个业务逻辑,邮箱地址域名如果是@foodcome.com,这说明是平台上注册的餐馆商家,否则,就是普通顾客。
这样,我们就需要在用户修改邮箱的时候,加入一个判断,如果邮箱地址的修改是从普通域到@FoodCome域,商家在平台里的数量就+1,否则,就-1。
这个需求看起来很简单,是吧。但是我要告诉你,即使这么简单的逻辑,也需要做好设计,否则,写出来的代码可能完全无法单元测试,不信就来看看吧。
版本V1
新建了一个User类,在User类里有一个changeEmail的方法,负责修改邮箱地址。这是第一版代码User.java下的内容:
package com.mockito.learn.junit.v1;
import com.mockito.learn.junit.v1.enums.UserType;
import com.mockito.learn.junit.v1.utils.Database;
import com.mockito.learn.junit.v1.utils.MessageBus;
/**
*通过代码走读可以发现,Database,MessageBus工具类中的方法是静态方法。静态方法在单元测试中的坏处:
* 使用 static 方法是一种主动获取的做法。一旦组件主动获取,测试就没有机会参与到其中,相应地,我们也就控制不了相应的行为,测试难度自然就增大了。
* 所以,在写代码时候尽量不使用 static 方法。
*/
public class User {
private int UserId;
private String Email;
public UserType Type;
public void ChangeEmail(int userId, String newEmail) {
//查询出来用户的ID,email和type
Object[] data = Database.GetUserById(userId);
UserId = userId;
Email = (String) data[1];
Type = (UserType) data[2];
//如果和修改前的email相同,直接返回
if (Email == newEmail)
return;
//从数据库里获得商家的数量
int numberOfRestaurtants = Database.GetRestaurant();
//判断用户要修改的类型是商家还是顾客
String emailDomain = newEmail.split("@")[1];
boolean isEmailRestaurant = emailDomain == "foodcome.com";
UserType newType = isEmailRestaurant
? UserType.Restaurant
: UserType.Customer;
//如果是修改成为商家,那么商家数量+1,如果是修改为顾客,商家数量-1
if (Type != newType) {
int delta = newType == UserType.Restaurant ? 1 : -1;
int newNumber = numberOfRestaurtants + delta;
Database.SaveRestaurant(newNumber);
MessageBus.SendEmailChangedMessage(UserId, newEmail);
}
//提交用户信息修改,入库
Database.SaveUser(this);
}
}
package com.mockito.learn.junit.v1.utils;
import com.mockito.learn.junit.v1.User;
public class Database {
public static Object[] GetUserById(int userId) {
return null;
}
public static int GetRestaurant() {
return 0;
}
public static void SaveRestaurant(int newNumber) {
}
public static void SaveUser(User user) {
}
}
package com.mockito.learn.junit.v1.utils;
public class MessageBus {
public static void SendEmailChangedMessage(int userId, String newEmail) {
}
}
package com.mockito.learn.junit.v1.enums;
public enum UserType {
Customer(1),
Restaurant(2),
;
UserType(Integer code) {
this.code = code;
}
private Integer code;
}
上面的代码并不复杂,主要功能是判断邮件地址是否为FoodCome,然后更新数据库,发送通知到消息总线上。
存在的问题
使用了 static 方法
通过代码走读可以发现,Database,MessageBus工具类中的方法是静态方法。静态方法在单元测试中的坏处:
使用 static 方法是一种主动获取的做法。一旦组件主动获取,测试就没有机会参与到其中,相应地,我们也就控制不了相应的行为,测试难度自然就增大了。所以,在写代码时候尽量不使用 static 方法。
如果你能够摒弃掉 static 方法,那么全局状态、Singleton 模式也尽量不要用。
-
如果系统中有全局状态,那就会造成代码之间彼此的依赖:一段代码改了状态,另一端代码因为要使用这个状态而崩溃。
-
如果系统中有Singleton 模式。它也是一样没有办法去干涉对象的创建,而且它本身限制了继承,也没有办法去模拟。
之所以说编写可组合的代码是可测试性的关键,是因为我们在测试的过程中要参与到组件的组装过程中,我们可能会用模拟对象代替真实对象。模拟对象对我们来说是完全可控的,而真实对象则不一定那么方便,比如真实对象可能会牵扯到外部资源,带来的问题可能比解决的问题更多。
版本V2
package com.mockito.learn.junit.v2;
import com.mockito.learn.junit.v2.enums.UserType;
import com.mockito.learn.junit.v2.utils.Database;
import com.mockito.learn.junit.v2.utils.MessageBus;
/**
* 不要在组件内部去创建对象
*
*
* ChangeEmail(){
* Database database = new Database();
* MessageBus messageBus = new MessageBus();
* }
* 通过代码可以发现ChangeEmail方法内创建了对象Database、MessageBus。比如要直接创建数据库访问的对象Database,
* 要创建数据库访问对象,就要同时把数据库连接起来,你要准备一大堆相关的东西,所以,测试的复杂度就会非常大。而且测试ChangeEmail()的目的是测试
* 该方法业务逻辑是否正确,与链接数据库和消息推送系统无关。解决方法见v3
*
*
*/
public class User {
private int UserId;
private String Email;
public UserType Type;
public void ChangeEmail(int userId, String newEmail) {
Database database = new Database();
MessageBus messageBus = new MessageBus();
//查询出来用户的ID,email和type
Object[] data = database.GetUserById(userId);
UserId = userId;
Email = (String) data[1];
Type = (UserType) data[2];
//如果和修改前的email相同,直接返回
if (Email == newEmail)
return;
//从数据库里获得商家的数量
int numberOfRestaurtants = database.GetRestaurant();
//判断用户要修改的类型是商家还是顾客
String emailDomain = newEmail.split("@")[1];
boolean isEmailRestaurant = emailDomain == "foodcome.com";
UserType newType = isEmailRestaurant
? UserType.Restaurant
: UserType.Customer;
//如果是修改成为商家,那么商家数量+1,如果是修改为顾客,商家数量-1
if (Type != newType) {
int delta = newType == UserType.Restaurant ? 1 : -1;
int newNumber = numberOfRestaurtants + delta;
database.SaveRestaurant(newNumber);
messageBus.SendEmailChangedMessage(UserId, newEmail);
}
//提交用户信息修改,入库
database.SaveUser(this);
}
}
通过代码可以发现ChangeEmail方法内创建了对象Database、MessageBus。比如要直接创建数据库访问的对象Database,要创建数据库访问对象,就要同时把数据库连接起来,你要准备一大堆相关的东西,所以,测试的复杂度就会非常大。而且测试ChangeEmail()的目的是测试该方法业务逻辑是否正确,与链接数据库和消息推送系统无关。
ChangeEmail(){
Database database = new Database();
MessageBus messageBus = new MessageBus();
//...
}
所以不要在组件内部创建对象
存在的问题
组件内部创建对象
在通常的架构中,服务会调用数据库访问的代码。如果是不考虑测试的做法,代码可能写成这样:
class ProductService {
// 访问数据库的对象
private ProduceRepository repository = new ProductRepository();
public Product find(final long id) {
return this.repository.find(id);
}
}
在这里,我们要直接创建数据库访问的对象,然而,要创建数据库访问对象,就要同时把数据库连接起来,你要准备一大堆相关的东西,所以,测试的复杂度就会非常大。
如果考虑了可测试性,服务的依赖就变成了一个数据访问的接口:
class ProductService {
// 访问数据库的对象
private ProduceRepository repository;
public ProductService(final ProduceRepository repository) {
this.repository = repository;
}
public Product find(final long id) {
return this.repository.find(id);
}
}
不在内部创建,那就意味着把组件的组装过程外置了。既然是外置了,组装的活可以由产品代码完成,同样也可以由测试过程完成。
站在测试的角度看,如果我们需要测试 ProductService 就不需要依赖于 repository 的具体实现,完全可以使用模拟对象进行替代。
我们可以完全控制模拟对象的行为,这样,对 ProductService 的测试重点就全在 ProductService 本身,无需考虑 repository 的实现细节。
不在内部创建对象,那谁来负责这个对象的创建呢?答案是组件的组装过程。组件组装在 Java 世界里已经有了一个标准答案,就是依赖注入。
版本V3
考虑到可测试性,服务的依赖就变成了一个数据访问的接口:
class ProductService {
// 访问数据库的对象
private ProduceRepository repository;
public ProductService(final ProduceRepository repository) {
this.repository = repository;
}
public Product find(final long id) {
return this.repository.find(id);
}
}
不在内部创建对象,那就意味着把组件的组装过程外置了。既然是外置了,组装的活可以由产品代码完成,同样也可以由测试过程完成。
V3版本代码
package com.mockito.learn.junit.v3.domain;
import com.mockito.learn.junit.v3.enums.UserType;
import com.mockito.learn.junit.v3.utils.Database;
import com.mockito.learn.junit.v3.utils.MessageBus;
/**
* 考虑可测试性,需要将User#ChangeEmail()方法对对象Database、MessageBus的依赖变成对接口的依赖,那就意味着把组件的组装过程外置了。
* 既然代码的组装外置了,那么可以将User#ChangeEmail()对Database database;MessageBus messageBus的依赖,提到UserService中。
* <p>
*
* User#ChangeEmail()方法里只是对邮件地址做判断,并计算新的餐馆数量,将新值返回,不会涉及到对MessageBus messageBus的依赖。User Class可以测试了,因为不需要做任何Mock。
*
* public class User {
* private int UserId;
* private String Email;
* private UserType Type;
* Database database;
* MessageBus messageBus;
* }
* 领域层的代码可以只专注于业务逻辑实现。
* <p>
* public class User {
* private int UserId;
* private String Email;
* private UserType Type;
* }
*
* <p>
* 代码的组装可以交给产品代码。那产品代码就可以只专注于代码的组装工作和管理依赖
*
* <p>
*
* UserService{
* private Database database;
* private MessageBus messageBus;
* //....
* }
*
* 如果我们需要测试 UserService#ChangeEmail()方法,那么 UserService#ChangeEmail()方法中对 Database、MessageBus的依赖, 就不需要依赖于 Database、MessageBus
* 的具体实现,完全可以使用模拟对象进行替代。这样的好处就是,可以完全控制模拟对象的行为,
*
* 这样,对 UserService#ChangeEmail()方法的测试重点就全在 UserService#ChangeEmail()方法本身,无需考虑 Database、MessageBus 的实现细节。
*
* 存在的问题
* 1. UserService里实例化了User class,这包含了业务逻辑(构造一个User对象),而我们希望UserService更为纯粹,专注依赖管理;
* 2. User class和Restaurant数据也存在耦合,通过User Class的ChangeEmail函数返回了Restaurant的newNumofRestaurant,这和User的行为模型毫无关系。
*
*
*/
public class User {
private int UserId;
private String Email;
private UserType Type;
public User(int userId, String email, UserType type) {
UserId = userId;
Email = email;
Type = type;
}
public int ChangeEmail(String newEmail, String companyDomainName, int numberOfRestaurants) {
if (Email == newEmail)
return numberOfRestaurants;
String emailDomain = newEmail.split("@")[1];
boolean isEmailCorporate = emailDomain == companyDomainName;
UserType newType = isEmailCorporate
? UserType.Restaurant
: UserType.Customer;
if (Type != newType) {
int delta = newType == UserType.Restaurant ? 1 : -1;
numberOfRestaurants = numberOfRestaurants + delta;
}
return numberOfRestaurants;
}
}
那么User Class更改成
public class User {
private int UserId;
private String Email;
private UserType Type;
Database database;
MessageBus messageBus;
}
考虑可测试性,需要将User#ChangeEmail()方法对对象Database、MessageBus的依赖变成对接口的依赖,那就意味着把组件的组装过程外置了。
既然代码的组装外置了,那么可以将User#ChangeEmail()对MessageBus messageBus的依赖,提到UserService中。
把 Database,MessageBus 提到application层,那么User Class的代码为
public class User {
private int UserId;
private String Email;
private UserType Type;
}
User class 代码
public class User {
private int UserId;
private String Email;
private UserType Type;
public User(int userId, String email, UserType type) {
UserId = userId;
Email = email;
Type = type;
}
public int ChangeEmail(String newEmail, String companyDomainName, int numberOfRestaurants) {
if (Email == newEmail)
return numberOfRestaurants;
String emailDomain = newEmail.split("@")[1];
boolean isEmailCorporate = emailDomain == companyDomainName;
UserType newType = isEmailCorporate
? UserType.Restaurant
: UserType.Customer;
if (Type != newType) {
int delta = newType == UserType.Restaurant ? 1 : -1;
numberOfRestaurants = numberOfRestaurants + delta;
}
return numberOfRestaurants;
}
}
User#ChangeEmail()方法里只是对邮件地址做判断,并计算新的餐馆数量,将新值返回,不会涉及到对MessageBus messageBus的依赖。User Class可以测试了,因为不需要做任何Mock。
领域层的代码可以只专注于业务逻辑实现。
UserService class 代码
代码的组装可以交给Application层。那Application层就可以只专注于代码的组装工作和管理依赖
UserService{
private Database database;
private MessageBus messageBus;
//....
}
如果我们需要测试 UserService#ChangeEmail()方法,那么 UserService#ChangeEmail()方法中对 Database、MessageBus的依赖, 就不需要依赖于 Database、MessageBus的具体实现,完全可以使用模拟对象进行替代。这样的好处就是,可以完全控制模拟对象的行为,这样,对 UserService#ChangeEmail()方法的测试重点就全在 UserService#ChangeEmail()方法本身,无需考虑 Database、MessageBus 的实现细节。
package com.mockito.learn.junit.v3.application;
import com.mockito.learn.junit.v3.domain.User;
import com.mockito.learn.junit.v3.enums.UserType;
import com.mockito.learn.junit.v3.utils.Database;
import com.mockito.learn.junit.v3.utils.MessageBus;
public class UserService {
private Database database;
private MessageBus messageBus;
public UserService(Database database, MessageBus messageBus) {
this.database = database;
this.messageBus = messageBus;
}
//changeEmail方法里调用依赖
public void ChangeEmail(int userId, String newEmail) {
Object[] data = database.GetUserById(userId);
String email = (String) data[1];
UserType type = (UserType) data[2];
// todo: 问题点一: UserController里实例化了User class
User user = new User(userId, email, type);
int numberOfRestaurants = database.GetRestauruant();
String companyDomainName = "foodcome.com";
//调用User的changeEmail,返回变化的商家数量。
int newNumberOfRestaurants = user.ChangeEmail(
newEmail, companyDomainName, numberOfRestaurants);
//调用依赖,变更数据库和发布消息
if (newNumberOfRestaurants != numberOfRestaurants) {
database.SaveCompany(newNumberOfRestaurants);
database.SaveUser(user);
messageBus.SendEmailChangedMessage(userId, newEmail);
}
}
}
V3版本代码的优点
- 领域层不负责任何依赖,只专注于业务逻辑实现
- Application层专注于代码的组装工作和管理依赖
- 相比于版本V1、V2,对Database和MessageBus的依赖从隐式依赖变成了显式成员变量
V3版本存在的问题
- UserService里实例化了User class,这包含了业务逻辑(构造一个User对象),而我们希望UserService更为纯粹,专注依赖管理;
- User class和Restaurant数据也存在耦合,通过User Class的ChangeEmail函数返回了Restaurant的newNumofRestaurant,这和User的行为模型毫无关系。
版本V4
V4代码
UserFactory
package com.mockito.learn.junit.v4.domain.factory;
import com.mockito.learn.junit.v4.domain.User;
import com.mockito.learn.junit.v4.enums.UserType;
public class UserFactory
{
public static User Create(Object[] data)
{
Precondition.Requires(data.length >= 3);
int id = (int)data[0];
String email = (String)data[1];
UserType type = (UserType)data[2];
return new User(id, email, type);
}
}
可以看到UserFactory也是可以简单测试的
RestaurantFactory
package com.mockito.learn.junit.v4.domain.factory;
import com.mockito.learn.junit.v4.domain.Restaurant;
public class RestaurantFactory {
public static Restaurant Create(Object[] restaurantData) {
return new Restaurant();
}
}
可以看到RestaurantFactory也是可以简单测试的
Restaurant
package com.mockito.learn.junit.v4.domain;
import com.mockito.learn.junit.v4.domain.factory.Precondition;
public class Restaurant {
public String DomainName;
public int NumberOfRestaurant;
public void ChangeNumberOfRestaurants(int delta) {
Precondition.Requires(NumberOfRestaurant + delta >= 0);
this.NumberOfRestaurant += delta;
}
public boolean IsEmailCorporate(String email) {
String emailDomain = email.split("@")[1];
return emailDomain == DomainName;
}
public boolean numberChanged() {
return false;
}
}
Restaurant Class也是可以简单测试的!
User
package com.mockito.learn.junit.v4.domain;
import com.mockito.learn.junit.v4.enums.UserType;
import com.mockito.learn.junit.v4.utils.Database;
import com.mockito.learn.junit.v4.utils.MessageBus;
/**
* 考虑了可测试性, ChangeEmail方法对对象Database、MessageBus的依赖变成了对接口的依赖,那就意味着把组件的组装过程外置了。
* 既然是外置了,组装的活可以由产品代码完成,同样也可以由测试过程完成。
* <p>
* 站在开发的角度看,完善 ChangeEmail方法中对 Database、MessageBus的依赖,可以在组装的活可以由产品代码完成。
* <p>
* 站在测试的角度看,如果我们需要测试 ChangeEmail方法,那么ChangeEmail方法中对 Database、MessageBus的依赖, 就不需要依赖于 Database、MessageBus
* 的具体实现,完全可以使用模拟对象进行替代。这样的好处就是,可以完全控制模拟对象的行为,
* 这样,对 ChangeEmail方法的测试重点就全在 ChangeEmail方法本身,无需考虑 Database、MessageBus 的实现细节。
*/
public class User {
private int UserId;
private String Email;
private UserType Type;
public User(int userId, String email, UserType type) {
UserId = userId;
Email = email;
Type = type;
}
public void ChangeEmail(String newEmail, Restaurant restaurant) {
if (Email == newEmail)
return;
UserType newType = restaurant.IsEmailCorporate(newEmail)
? UserType.Restaurant
: UserType.Customer;
if (Type != newType) {
int delta = newType == UserType.Restaurant ? 1 : -1;
restaurant.ChangeNumberOfRestaurants(delta);
}
}
}
是可以简单测试的!
UserService
而UserService更关注与外部依赖的管理和交互
package com.mockito.learn.junit.v4.application;
import com.mockito.learn.junit.v4.domain.Restaurant;
import com.mockito.learn.junit.v4.domain.User;
import com.mockito.learn.junit.v4.domain.factory.RestaurantFactory;
import com.mockito.learn.junit.v4.domain.factory.UserFactory;
import com.mockito.learn.junit.v4.utils.Database;
import com.mockito.learn.junit.v4.utils.MessageBus;
public class UserService {
private Database database;
private MessageBus messageBus;
public UserService(Database database, MessageBus messageBus) {
this.database = database;
this.messageBus = messageBus;
}
//changeEmail方法里调用依赖
public void ChangeEmail(int userId, String newEmail) {
Object[] data = database.GetUserById(userId);
User user = UserFactory.Create(data);
Object[] restaurantData = database.GetRestaurant();
Restaurant restaurant = RestaurantFactory.Create(restaurantData);
user.ChangeEmail(newEmail, restaurant);
database.SaveUser(user);
//如果restaurant数量有变化,就写数据库,发送通知信息
if (restaurant.numberChanged()) {
database.SaveRestaurant(restaurant);
messageBus.SendEmailChangedMessage(userId, newEmail);
}
}
}
UserService Class里没有业务逻辑,只有Database和MessageBus的管理和操作。在单元测试时,只需要用Mock替代,就OK了!
5. 总结
编写可测试的代码,如果只记住一个通用规则,那就是编写可组合的代码。什么叫可组合的代码?就是要能够像积木一样组装起来的代码。
由此可见,可测试性差的代码,本身代码设计得也不够好,很多地方都没有遵守设计原则和思想,比如“基于接口而非实现编程”思想、依赖反转原则等。代码的可测试性可以从侧面上反应代码设计是否合理。除此之外,在平时的开发中,我们也要多思考一下,这样编写代码,是否容易编写单元测试,这也有利于我们设计出好的代码。
1. 什么是代码的可测试性?
所谓代码的可测试性,就是针对代码编写单元测试的难易程度。对于一段代码,如果很难为其编写单元测试,或者单元测试写起来很费劲,需要依靠单元测试框架中很高级的特性,那往往就意味着代码设计得不够合理,代码的可测试性不好。
2. 编写可测试性代码的最有效手段
依赖注入是编写可测试性代码的最有效手段。通过依赖注入,我们在编写单元测试的时候,可以通过 mock 的方法解依赖外部服务,这也是我们在编写单元测试的过程中最有技术挑战的地方。