返回介绍

13.4 应用测试挡板(Test Doubles)

发布于 2025-04-22 20:10:04 字数 11494 浏览 0 评论 0 收藏

被测系统(SUT)很少孤立存在。通常为了测试一个类,你需要依赖。在测试中,你的 SUT 所需要的依赖称为协作者。

协作者经常被称为测试挡板 的其他对象取代。测试挡板是 Gerard Meszaros 在他的书《xUnit Test Patterns: Refaltoning your code》中创造的一个术语。他对该术语的解释可以在这个网页中找到:http://xunitpatterns.com/Test%20Double.html

使用测试挡板有几个原因。

  • 在编写测试类时,真正的依赖还没有准备好。

  • 一些依赖项,例如 HttpServletRequest 和 HttpServletResponse 对象,是从 servlet 容器获取的,而自己创建这些对象将会非常耗时。

  • 一些依赖关系启动和初始化速度较慢。例如,DAO 对象访问数据库导致单元测试执行很慢。

测试挡板在单元测试中广泛使用,也用于集成测试。当前有许多用于创建测试挡板的框架。Spring 也有自己的类来创建测试挡板。

模拟框架可用于创建测试挡板和验证代码行为。这里有一些流行的框架。

  • Mockito。

  • EasyMock。

  • jMock。

除了上面的库,Spring 还附带了创建模拟对象的类。在下一节中,我们将使用 Spring 类来测试 Spring MVC 控制器。在本节中,我们将学习使用 Mockito。

Mockito 无法直接下载,所以你必须使用 Maven。但是,本章的示例项目包括 Mockito 发布包(一个 mockito.jar 文件)及其依赖(一个 objenesis.jar 文件)。

要使用 Maven 下载 Mockito,请将以下依赖关系添加到 pom.xml 文件中。

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>2.0.43-beta</version>
  <type>pom</type> 
</dependency>

在开始写测试挡板之前,你应该先学习理论知识。如下是测试挡板的 5 种类型。

  • dummy;

  • stub;

  • spy;

  • fake;

  • mock。

这些类型中的每一种将在下面的小节中解释。

13.4.1 dummy

dummy 是最基本的测试挡板类型。一个 dummy 是一个协作者的实现,它不做任何事情,并且不改变 SUT 的行为。它通常用于使 SUT 可以实例化。dummy 只在开发的早期阶段使用。

例如,考虑清单 13.6 中的 ProductServiceImpl 类。这个类依赖于传递给构造函数的 ProductDAO。

清单 13.6 ProductServiceImpl 类

package com.example.service; 
import com.example.dao.ProductDAO; 

public class ProductServiceImpl implements ProductService { 

  private ProductDAO productDAO; 

  public ProductServiceImpl(ProductDAO productDAOArg) { 
    if (productDAOArg == null) { 
      throw new NullPointerException("ProductDAO cannot be null."); 
    } 
    this.productDAO = productDAOArg; 
  } 

  @Override
  public BigDecimal calculateDiscount() { 
    return productDAO.calculateDiscount(); 
  } 
  @Override
  public boolean isOnSale(int productId) { 
    return productDAO.isOnSale(productId); 
  }
}

ProductServiceImpl 类需要一个非空的 ProductDAO 对象来实例化。同时,要测试的方法不使用 ProductDAO。因此,可以创建一个如清单 13.7 所示的 dummy 对象,只需让 ProductServiceImpl 可实例化。

清单 13.7 ProductDAODummy 类

package com.example.dummy;
import java.math.BigDecimal; 
import com.example.dao.ProductDAO; 

public class ProductDAODummy implements ProductDAO { 
  public BigDecimal calculateDiscount() { 
    return null; 
  } 
  public boolean isOnSale(int productId) { 
    return false; 
  };
}

在 dummy 类中的方法实现什么也不做,它的返回值也不重要,因为这些方法从未使用过。

清单 13.8 显示了一个可以运行的测试类

清单 13.8 ProductDAOTest 类

package com.example.dummy;
import static org.junit.Assert.assertNotNull; 
import org.junit.Test;
import com.example.dao.ProductDAO;
import com.example.service.ProductService; 
import com.example.service.ProductServiceImpl; 

public class ProductServiceImplTest { 

  @Test
  public void testCalculateDiscount() { 
    ProductDAO productDAO = new ProductDAODummy(); 
    ProductService productService = 
        new ProductServiceImpl(productDAO); 
    assertNotNull(productService); 
  } 
}

13.4.2 stub

像 dummy 一样,stub 也是依赖接口的实现。和 dummy 不同的是,stub 中的方法返回硬编码值,并且这些方法被实际调用。

清单 13.9 显示了一个 stub,可用于测试清单 13.6 中的 ProductServiceImpl 类。

清单 13.9 ProductDAOStub 类

package com.example.stub; 
import java.math.BigDecimal; 
import com.example.dao.ProductDAO; 

public class ProductDAOStub implements ProductDAO { 
  public BigDecimal calculateDiscount() { 
    return new BigDecimal(14); 
  } 
  public boolean isOnSale(int productId) { 
    return false; 
  };
}

13.4.3 spy

spy 是一个略微智能一些的 stub,因为 spy 可以保留状态。考虑下面的汽车租赁应用程序,其中包含一个 GarageService 接口和一个 GarageServiceImpl 类,分别在清单 13.10 和清单 13.11 中。

清单 13.10 GarageService 接口

package com.example.service; 
import com.example.Car; 

public interface GarageService { 
  Car rent(); 
}

清单 13.11 GarageServiceImpl 类

package com.example.service; 
import com.example.Car;
import com.example.dao.GarageDAO; 

public class GarageServiceImpl implements GarageService { 
  private GarageDAO garageDAO;
  public GarageServiceImpl(GarageDAO garageDAOArg) { 
    this.garageDAO = garageDAOArg; 
  } 
  public Car rent() {
    return garageDAO.rent(); 
  }
}

GarageService 接口只有一个方法:rent。GarageServiceImpl 类是 GarageService 的一个实现,并且依赖一个 GarageDAO。GarageServiceImpl 中的 rent 方法调用 GarageDAO 中的 rent 方法。GarageDAO 的实现应该返回一个 car,如果还有一辆汽车在车库;或者返回 null,如果没有更多的汽车。

由于 GarageDAO 的真正实现还没有完成,清单 13.12 中的 GarageDAOSpy 类被用作测试挡板。它是一个 spy,因为它的方法返回一个硬编码值,并且它通过一个 carCount 变量来保存车库里的车数。

清单 13.12 GarageDAOSpy 类

package com.example.spy;
import com.example.Car;
import com.example.dao.GarageDAO; 

public class GarageDAOSpy implements GarageDAO { 
  private int carCount = 3; 

  @Override
  public Car rent() { 
    if (carCount == 0) { 
      return null; 
    } else { 
      carCount--; 
      return new Car(); 
    } 
  } 
}

清单 13.13 显示了使用 GarageDAOSpy 测试 GarageServiceImpl 类的一个测试类。

清单 13.13 GarageServiceImplTest 类

package com.example.spy;
import com.example.Car;
import com.example.dao.GarageDAO;
import com.example.service.GarageService; 
import com.example.service.GarageServiceImpl; 
import org.junit.Test;
import static org.junit.Assert.*; 

public class GarageServiceImplTest { 

  @Test
  public void testRentCar() { 
    GarageDAO garageDAO = new GarageDAOSpy();
    GarageService garageService = new GarageServiceImpl(garageDAO); 
    Car car1 = garageService.rent();
    Car car2 = garageService.rent();
    Car car3 = garageService.rent();
    Car car4 = garageService.rent();

    assertNotNull(car1);
    assertNotNull(car2); 
    assertNotNull(car3); 
    assertNull(car4); 
  }
}

由于在车库只有 3 辆车,spy 只能返回 3 辆车,当第四次调用其 rent 方法时,返回 null。

13.4.4 fake

fake 的行为就像一个真正的合作者,但不适合生产,因为它走“捷径”。内存存储是一个 fake 的完美示例,因为它的行为像一个 DAO,但不会将其状态保存到硬盘驱动器。

例如,分别考虑清单 13.14 和清单 13.15 中的 Member 和 MemberServiceImpl 类。Member 类包含成员的标识符和名称。MemberServiceImpl 类可以将成员添加到商店并检索所有存储的成员。

清单 13.14 Member 类

package com.example.model; 
public class Member { 
  private int id;
  private String name;
  public Member(int idArg, String nameArg) { 
    this.id = idArg; 
    this.name = nameArg; 
  } 
  public int getId() { 
    return id; 
  }
  public void setId(int idArg) { 
    this.id = idArg; 
  } 
  public String getName() { 
    return name; 
  }
  public void setName(String nameArg) { 
    this.name = nameArg;
  } 
}

清单 13.15 MemberServiceImpl 类

package com.example.service; 
import java.util.List;
import com.example.dao.MemberDAO; 
import com.example.model.Member; 

public class MemberServiceImpl implements MemberService { 
  private MemberDAO memberDAO;
  public void setMemberDAO(MemberDAO memberDAOArg) { 
    this.memberDAO = memberDAOArg; 
  } 

  @Override
  public void add(Member member) { 
    memberDAO.add(member); 
  } 
  @Override
  public List<Member> getMembers() { 
    return memberDAO.getMembers(); 
  } 
}

MemberServiceImpl 依赖于 MemberDAO。但是,由于没有可用的 MemberDAO 实现,你可以创建一个 MemberDAO 的 fake 实现,以便可以立即测试 MemberServiceImpl。清单 13.16 显示了这样一个 fake。它将成员存储在 ArrayList 中,而不是持久化存储。因此,不能在生产中使用它,但是对于单元测试是足够的。

清单 13.16 MemberDAOFake 类

package com.example.fake;
import java.util.ArrayList;
import java.util.List;
import com.example.dao.MemberDAO; 
import com.example.model.Member; 

public class MemberDAOFake implements MemberDAO { 
  private List<Member> members = new ArrayList<>(); 

  @Override
  public void add(Member member) { 
    members.add(member); 
  } 

  @Override
  public List<Member> getMembers() { 
    return members; 
  } 
}

清单 13.17 显示了一个测试类,它使用 MemberDAOFake 作为 MemberDAO 的测试挡板类来测试 MemberServiceImpl。

清单 13.17 MemberServiceImplTest 类

package com.example.service;
import org.junit.Assert;
import org.junit.Test;
import com.example.dao.MemberDAO; 
import com.example.fake.MemberDAOFake; 
import com.example.model.Member; 

public class MemberServiceImplTest { 

  @Test
  public void testAddMember() { 
    MemberDAO memberDAO = new MemberDAOFake();
    memberDAO.add(new Member(1, "John Diet")); 
    memberDAO.add(new Member(2, "Jane Biteman")); 
    Assert.assertEquals(2, memberDAO.getMembers().size()); 
  } 
}

13.4.5 mock

mock 在理念上不同于其他测试挡板。使用 dummy、stub、spy 和 fake 来进行状态测试,即验证方法的输出。而使用 mock 来执行行为(交互)测试,以确保某个方法真正被调用,或者验证一个方法在执行另一个方法期间被调用了一定的次数。

例如,考虑清单 13.18 中的 MathUtil 类。

清单 13.18 MathUtil 类

package com.example; 
public class MathUtil { 
  private MathHelper mathHelper;
  public MathUtil(MathHelper mathHelper) { 
    this.mathHelper = mathHelper; 
  } 
  public MathUtil() { 

  } 

  public int multiply(int a, int b) { 
    int result = 0; 
    for (int i = 1; i <= a; i++) {
      result = mathHelper.add(result, b); 
    } 
    return result; 
  } 
}

MathUtil 有一个方法 multiply。它非常直接,使用多个 add 方法。换句话说,3 × 8 计算为 8 + 8 + 8。MathUtil 类甚至不知道如何执行 add。为此,它依赖于 MathHelper 对象,其类在列表 13.19 中结出。

清单 13.19 MathHelper 类

package com.example; 
public class MathHelper { 
  public int add(int a, int b) { 
    return a + b; 
  } 
}

测试所关心的不是 multiply 方法的结果,而是找出方法是否如预期一样运行。因此,在计算 3 × 8 时,它应该调用 MathHelper.add() 3 次。清单 13.20 显示了一个使用 MathHelper 模拟的测试类。Mockito 是一个流行的模拟框架,用于创建模拟对象。我将在本章稍后介绍关于 Mockito 的更多知识。在这一节中,我只是想告诉你这个概念。

清单 13.20 MathUtilTest 类

package com.example;
import static org.mockito.Mockito.mock; 
import static org.mockito.Mockito.times; 
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 
import org.junit.Test; 

public class MathUtilTest { 

  @Test
  public void testMultiply() { 
    MathHelper mathHelper = mock(MathHelper.class); 
    for (int i = 0; i < 10; i++) { 
      when(mathHelper.add(i * 8, 8)).thenReturn(i * 8 + 8); 
    } 
    MathUtil mathUtil = new MathUtil(mathHelper); 
    mathUtil.multiply(3, 8);
    verify(mathHelper, times(1)).add(0, 8); 
    verify(mathHelper, times(1)).add(8, 8); 
    verify(mathHelper, times(1)).add(16, 8); 
  } 
}

使用 Mockito 创建 mock 对象非常简单,只需调用 org.mockito.Mockito 类的静态 mock 方法即可。下面展示如何创建 MathHelper mock 对象。

MathHelper mathHelper = mock(MathHelper.class);

接下来,你需要使用 when 方法准备 mock 对象。基本上,你告诉它,给定使用这组参数的方法调用,mock 对象必须返回这个值。例如,这条语句是说如果调用 mathHelper.add(10,20),返回值必须是 10 + 20:

when(mathHelper.add(10,20) ).thenReturn(10 + 20);

对于此测试,你准备具有十组参数的 mock 对象(但不是所有的参数组都会被使用)。

for (int i = 0; i < 10; i++) {
  when(mathHelper.add(i * 8, 8)).thenReturn(i * 8 + 8); 
}

然后创建要测试的对象并调用其参数。

MathUtil mathUtil = new MathUtil(mathHelper); 
mathUtil.multiply(3,8);

接下来的 3 条语句是行为测试。为此,调用 verify 方法:

verify(mathHelper,times(1)).add(0,8); 
verify(mathHelper,times(1)).add(8,8); 
verify(mathHelper,times(1)).add(16,8);

第一条语句验证 mathHelper.add(0,8)被调用了一次。第二条语句验证 mathHelper.add(8,8)被调用一次,第三条语句验证 mathHelper.add(16,8)也被调用一次。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。