单元测试由谁来完成_您已完成单元测试; 您的测试刚刚开始

单元测试由谁来完成

如今,单元测试已经变得非常普遍,不进行单元测试的开发人员则感到羞愧。 维基百科将单元测试定义为:

一种软件测试方法,通过测试各个源代码...来确定它们是否适合使用。

在面向对象的语言中,特别是在Java中,一般用法是“源代码单元”是方法。 为了说明这一点,让我们使用Spring标志性的Pet Clinic应用程序中的一个简单示例。 为方便起见,此处复制了PetController类的摘录:

@Controller
@SessionAttributes("pet")
public class PetController {

    private final ClinicService clinicService;

    @Autowired
    public PetController(ClinicService clinicService) {
        this.clinicService = clinicService;
    }

    @ModelAttribute("types")
    public Collection<PetType> populatePetTypes() {
        return this.clinicService.findPetTypes();
    }

    @RequestMapping(value = "/owners/{ownerId}/pets/new", method = RequestMethod.GET)
    public String initCreationForm(@PathVariable("ownerId") int ownerId,
                                   Map<String, Object> model) {
        Owner owner = this.clinicService.findOwnerById(ownerId);
        Pet pet = new Pet();
        owner.addPet(pet);
        model.put("pet", pet);
        return "pets/createOrUpdatePetForm";
    }
    ...
}

如我们所见, initCreationForm()方法用于填充HTML表单以创建新的Pet实例。 我们的目标是针对以下情况测试此方法:

  1. Pet实例放入模型
  2. 设置此Pet实例的Owner
  3. 返回定义的视图

普通经典单元测试

如上所述,单元测试将需要对方法进行依赖。 一个基于流行的Mockito框架的典型单元测试如下所示:

public class PetControllerTest {

    private ClinicService clinicService;
    private PetController controller;

    @Before
    public void setUp() {
        clinicService = Mockito.mock(ClinicService.class);
        controller = new PetController(clinicService);
    }

    @Test
    public void should_set_pet_in_model() {
        Owner dummyOwner = new Owner();
        Mockito.when(clinicService.findOwnerById(1)).thenReturn(dummyOwner);
        HashMap<String, Object> model = new HashMap<String, Object>();
        controller.initCreationForm(1, model);
        Iterator<Object> modelIterator = model.values().iterator();
        Assert.assertTrue(modelIterator.hasNext());
        Object value = modelIterator.next();
        Assert.assertTrue(value instanceof Pet);
        Pet pet = (Pet) value;
        Owner petOwner = pet.getOwner();
        Assert.assertNotNull(petOwner);
        Assert.assertSame(dummyOwner, petOwner);
    }
}
  • setUp()方法初始化控制器以进行测试以及ClinicService依赖项。 由于它是依赖项,因此Mockito可以模拟它。
  • should_set_pet_in_model()检查在方法运行之后,该模型是否包含Pet实例,以及该模型是否具有所有者(由所有者ClinicService返回。
  • 请注意,未测试视图的返回,因为它将完全反映控制器的代码。

单元测试中缺少什么

至此,我们已经安全地达到了该方法的100%代码覆盖率,此时我们可以停止。 但是,在单元测试代码之后立即停止类似于在测试汽车的每个螺母和螺栓之后开始批量生产汽车。 当然,没有人会冒如此巨大的风险。 在现实生活中,首先要对汽车进行多次试驾,以检查不仅每个螺母和螺栓的组装,而且每个其他零件的组装是否都按预期进行了协调。 在软件开发世界中,测试驱动转化为我们亲切地称为集成测试的东西。 集成测试可确保类的协作有效。

在Java世界中,Spring框架和Java EE平台都是通过可用服务(例如用于数据库访问的JDBC)提供API的容器。 确保使用Spring或Java EE开发的应用程序能够按预期工作,要求它们在容器中进行测试以测试与容器提供的服务的交互。

在上面的测试示例中,某些东西未经测试-不能为:

  • Spring配置, 通过自动装配将所有类组装在一起
  • 在模型中populatePetTypes() PetType populatePetTypes()方法
  • URL映射到正确的控制器和方法, @RequestMapping批注
  • HTTP会话中Pet实例的设置, @SessionAttributes("pet")

容器内测试

集成测试及其专门的风味“容器内测试”是测试以上几点的解决方案。 幸运的是,Spring提供了一个针对此的完整测试框架,而Arquillian测试框架则愉快地包含了Java EE用户。 但是,Java EE应用程序具有不同的组装方法(例如CDI),而Arquillian提供了解决此类差异的方法。 在此背景下,让我们回到宠物诊所,并创建测试以验证以上几点。

Spring JUnit集成

顾名思义,JUnit是用于单元测试的框架。 Spring提供了专用的JUnit运行器,以在测试开始时启动Spring容器。 在测试类上使用@RunWith批注进行配置。

@RunWith(SpringJUnit4ClassRunner.class)
public class PetControllerIT { ... }

Spring框架有其自己的配置组件集。 旧版XML文件或更近期的Java“配置”类。 一个好的做法是拥有更细粒度的配置组件,以便人们可以选择并选择在测试环境中必需的那些组件。 可以使用测试类上的@ContextConfiguration批注来设置那些配置组件。

@ContextConfiguration("classpath:spring/business-config.xml")
public class PetControllerIT { ... }

最后,Spring允许基于称为profile的应用程序范围的标志来激活(或不激活)某些配置位。 使用配置文件就像在测试类上设置@ActiveProfile一样容易。

@ActiveProfiles("jdbc")
public class PetControllerIT { ... }

这些足以使用JUnit测试标准Spring Bean。 但是要测试Spring MVC控制器,需要付出更多的努力。

Spring Web上下文进行测试

对于Web应用程序,Spring在分层体系结构中以父子关系创建结构化上下文。 子级包含与Web相关的内容,例如控制器,格式化程序,资源包等。父级包含其余内容,例如服务和存储库。要模拟这种关系,请使用@ContextHierarchy批注注释测试类并进行配置它引用必要的@ContextConfiguration批注:

在以下测试程序片段中,business-config.xml是父级, mvc-core-config.xml是子级:

@ContextHierarchy({
    @ContextConfiguration("classpath:spring/business-config.xml"),
    @ContextConfiguration("classpath:spring/mvc-core-config.xml")
})

还需要设置@WebAppConfiguration批注以模拟WebApplicationContext而不是更简单的ApplicationContext

测试控制器

如上所述,一旦为测试设置了Web上下文,就可以测试控制器了。 入口是MockMvc类,它包含以下属性:

  • 创建假请求的请求生成器
  • 请求匹配器,用于检查控制器方法执行的结果
  • 结果处理程序结果执行任意处理

通过MockMvcBuilders类的静态方法可以使用MockMvc实例。 一个针对专用的控制器集,另一个针对整个应用程序上下文。 在后一种情况下,需要WebApplicationContext实例作为参数。 这非常容易:只需在测试类中自动连接此类型的属性。

@RunWith(SpringJUnit4ClassRunner.class)
public class PetControllerIT {

    @Autowired
    private WebApplicationContext context;
}

然后,通过perform(RequestBuilder)方法完成配置的MockMvc的执行。 反过来,可以通过MockMvcRequestBuilders类的静态方法使RequestBuilder实例可用。 每个静态方法都是一种执行特定HTTP方法的方法。

总结起来,这是一种模拟对/owners/1/pets/new路径的GET调用的方法。

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(PetController.class).build();
RequestBuilder getNewPet = MockMvcRequestBuilders.get("/owners/1/pets/new");
mockMvc.perform(getNewPet);

最后,Spring提供了大量的断言通过ResultMatcher界面和流畅度MockMvc API:饼干,内容,底层模型,HTTP状态代码,所有的这些都可以检查等等。

放在一起

现在,所有内容都可以用来测试早期的控制器,而仅通过单元测试就无法对其进行测试。

1 @RunWith(SpringJUnit4ClassRunner.class)
 2 @ContextHierarchy({
 3    @ContextConfiguration("classpath:spring/business-config.xml"),
 4   @ContextConfiguration("classpath:spring/mvc-core-config.xml")
 5 })
 6 @WebAppConfiguration
 7 @ActiveProfiles("jdbc")
 8 public class PetControllerIT {
 9
10    @Autowired
11    private WebApplicationContext context;
12
13    @Test
14    public void should_set_pet_types_in_model() throws Exception {
15        MockMvc mockMvc = webAppContextSetup(context).build();
16        RequestBuilder getNewPet = get("/owners/1/pets/new");
17        mockMvc.perform(getNewPet)
18            .andExpect(model().attributeExists("types"))
19            .andExpect(request().sessionAttribute("pet", instanceOf(Pet.class)));
20    }
21 }

这是这段代码中发生的事情:

  • 在第3和第4行,我们确保Spring配置文件配置正确
  • 在第16行,我们确保应用程序响应对表单准备URL的GET调用
  • 在第18行,我们测试模型中是否存在types属性
  • 最后,在第19行,我们测试会话中是否存在pet属性,并验证它是否属于预期的Pet类型

(请注意, instanceOf()静态方法来自Hamcrest API 。)

其他测试挑战

先前的PetClinic示例是一个简单的示例。 它已经开箱即用地使用了Hypersonic SQL数据库,可以自动创建数据库模式并插入一些数据,所有这些都在启动容器时完成。 在常规应用程序中,可能会在生产和测试期间使用不同的数据库。 同样,数据不会初始化用于生产。 挑战将包括如何切换到另一个数据库进行测试,如何在测试执行之前将数据库置于所需的状态以及如何在测试之后检查状态。

同样,PetController已针对单个initCreationForm()方法进行了测试,而创建过程还暗含了processCreationForm()方法。 为了减少测试初始化​​代码,不是测试每个方法而是测试用例本身并不是没有道理的。 这可能暗示着一种巨大的测试方法:如果测试失败,将很难找到失败的原因。 另一种方法是创建细粒度的正确命名的方法,并按顺序执行它们。 不幸的是,JUnit是一个真正的单元测试框架,不允许这样做。

与基础结构资源(例如数据库,邮件服务器,FTP服务器等)进行交互的每个组件都面临着相同的挑战:模拟该资源不会增加测试的价值。 例如,如何检查复杂的JPA查询? 这不仅需要模拟数据库。 通常的做法是建立专用的内存数据库。 根据上下文,可能会有更好的选择。 这种情况下的挑战是选择正确的替代方案并管理测试内部资源的生命周期。

随着基础结构资源的发展,Web服务构成了任何现代Web应用程序依赖项的很大一部分。 就当前的微服务趋势而言,情况甚至变得更糟。 如果一个人的应用程序依赖于外部Web服务,则必须测试这种应用程序与它的依赖关系之间的协作。 当然,Web服务依赖项的设置很大程度上取决于其性质,最常见的是SOAP或REST。

同样,如果该应用程序不是针对Spring而是针对Java EE,那么挑战也会变得不同。 Java EE提供了基于自动装配的上下文和依赖注入服务。 测试这样的应用程序意味着组装正确的组件集-类和配置文件。 此外,Java EE保证同一应用程序可以在不同的兼容应用程序服务器上运行。 如果应用程序具有不同的目标平台(例如,因为它是要部署到不同客户环境的产品),则必须对这种兼容性进行彻底的测试。

结论

在本文中,我以Spring MVC Web框架为例,展示了集成测试技术如何使您对代码更有信心。

我还简要介绍了测试如何带来一些挑战,这些挑战不能仅通过单元测试来解决,而需要集成测试。

这些是基本技术。 要更深入地了解其他技术和其他工具,请真正参考您所著的《从沟槽中进行集成测试》一书,在此我将介绍使用这些工具和技术以更好地保证运行软件的质量。

该书已在InfoQ上进行了审核,Leanpub上可以所有主要的电子格式提供,在Amazon可以以平装本获得。

翻译自: https://www.infoq.com/articles/Unit-Testing-Complete-Integration-Testing-Begins/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

单元测试由谁来完成

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值