作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Nikhil Bansal的头像

Nikhil Bansal

Nikhil (BTech)已经自动化了从J2EE到Elasticsearch到Kafka的所有金融部门代码. 当然,所有的东西都是AWS.

Previously At

Expedia Group
Share

在一个持续交付的时代, Java开发人员必须确信他们的更改不会破坏现有代码, hence automated testing. 有不止一种有效的方法,但你如何才能保持它们的正确性?

随着技术和行业的进步,从瀑布模型转向敏捷,现在又转向DevOps, 应用程序中的更改和增强在完成后立即部署到生产环境中. 代码部署到生产环境的速度如此之快, 我们需要确信我们的改变是有效的, 而且它们不会破坏任何先前存在的功能.

要建立这种信心,我们必须有 framework 用于自动回归测试. 进行回归测试, 从api级别的角度来看,应该执行许多测试, 但在这里,我们将介绍两种主要类型的测试:

  • Unit testing,其中任何给定的测试覆盖程序的最小单元(函数或过程)。. 它可以接受也可以不接受一些输入参数,也可以不返回一些值.
  • Integration testing, 单个单元一起测试以检查所有单元是否如预期的那样相互作用.

每种编程语言都有许多可用的框架. 我们将专注于编写单元和集成测试的web应用程序编写 Java’s Spring framework.

Most of the time, we write methods in a class, and these, in turn, 与其他类的方法交互. 在当今世界,尤其是在 enterprise applications-应用程序的复杂性是这样的,一个方法可能调用多个类的多个方法. 因此,在为这样的方法编写单元测试时, 我们需要一种从这些调用返回模拟数据的方法. 这是因为这个单元测试的目的是只测试一个方法,而不是测试这个特定方法所做的所有调用.


让我们在Spring中使用JUnit框架进行Java单元测试. 我们从一个你可能听说过的词开始:嘲讽.

什么是嘲讽,它什么时候出现?

Suppose you have a class, CalculateArea, which has a function calculateArea(类型类型,Double... args) 哪个计算给定类型的形状(圆形、正方形或矩形)的面积.)

在一个不使用依赖注入的正常应用程序中,代码是这样的:

公共类CalculateArea {

    SquareService SquareService;
    RectangleService RectangleService;
    CircleService CircleService;

    CalculateArea (SquareService SquareService, RectangleService rectangeService, CircleService CircleService)
    {
        this.squareService = squareService;
        this.rectangleService = rectangeService;
        this.circleService = circleService;
    }

    public Double calculateArea(类型类型,Double... r )
    {
        switch (type)
        {
            case RECTANGLE:
                if(r.length >=2)
                return rectangleService.area(r[0],r[1]);
                else
                    抛出新的RuntimeException("缺少必需参数");
            case SQUARE:
                if(r.length >=1)
                    return squareService.area(r[0]);
                else
                    抛出新的RuntimeException("Missing required param");

            case CIRCLE:
                if(r.length >=1)
                    return circleService.area(r[0]);
                else
                    抛出新的RuntimeException("Missing required param");
            default:
                抛出新的RuntimeException("不支持操作");
        }
    }
}
public class SquareService {

    public Double area(double r)
    {
        return r * r;
    }
}
公共类RectangleService {

    公共双区(双r,双h)
    {
        return r * h;
    }
}
public class CircleService {

    public Double area(Double r)
    {
        return Math.PI * r * r;
    }
}

public enum Type {

    RECTANGLE,SQUARE,CIRCLE;
}

现在,如果我们想对函数进行单元测试 calculateArea() of the class CalculateArea,那么我们的动机应该是检查是否 switch 用例和异常条件都有效. 我们不应该测试形状服务是否返回正确的值, because as mentioned earlier, 对函数进行单元测试的目的是测试函数的逻辑, 而不是函数调用的逻辑.

因此,我们将模拟各个服务函数返回的值(例如.g. rectangleService.area() 并测试调用函数(e).g. CalculateArea.calculateArea()).

矩形服务的一个简单测试用例 calculateArea() indeed calls rectangleService.area() 使用正确的参数-将看起来像这样:

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

公共类CalculateAreaTest {

    RectangleService RectangleService;
    SquareService SquareService;
    CircleService CircleService;

    CalculateArea CalculateArea;

    @Before
    public void init()
    {
        rectangleService = Mockito.mock(RectangleService.class);
        squareService = Mockito.mock(SquareService.class);
        circleService = Mockito.mock(CircleService.class);
        calculateArea = new calculateArea (squareService,rectangleService,circleService);
    }

    @Test
    公共void calculateRectangleAreaTest()
    {

        Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
        Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d);
        Assert.assertequal(新双(20 d), calculatedArea);

    }
}

这里需要注意两点:

  • rectangleService = Mockito.mock(RectangleService.class);-这将创建一个mock,它不是一个实际对象,而是一个模拟对象.
  • Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);-这句话是说,当被嘲笑时 rectangleService object’s area 方法使用指定的参数调用,然后返回 20d.

现在,当上面的代码是Spring应用程序的一部分时会发生什么?

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
公共类CalculateArea {

    SquareService SquareService;
    RectangleService RectangleService;
    CircleService CircleService;

    @Autowired SquareService, @Autowired RectangleService, @Autowired CircleService
    {
        this.squareService = squareService;
        this.rectangleService = rectangeService;
        this.circleService = circleService;
    }

    public Double calculateArea(类型类型,Double... r )
    {
        //(与之前相同的实现)
    }
}

这里我们有两个注释,底层Spring框架要在上下文初始化时检测:

  • @Component: Creates a bean of type CalculateArea
  • @Autowired: Searches for the beans rectangleService, squareService, and circleService 然后把它们注射到豆子里 calculatedArea

类似地,我们也为其他类创建bean:

import org.springframework.stereotype.Service;

@Service
public class SquareService {

    public Double area(double r)
    {
        return r*r;
    }
}

import org.springframework.stereotype.Service;

@Service
public class CircleService {

    public Double area(Double r)
    {
        return Math.PI * r * r;
    }
}
import org.springframework.stereotype.Service;

@Service
公共类RectangleService {

    公共双区(双r,双h)
    {
        return r*h;
    }
}

现在如果我们进行测试,结果是一样的. 我们在这里使用了构造函数注入,幸运的是,没有改变我们的JUnit测试用例.

但还有另一种方法来注入方寸之豆, circle, 矩形服务:字段注入. 如果我们使用它,那么我们的JUnit测试用例将需要一些小的更改.

我们不会深入讨论哪种注入机制更好, 因为这不在本文的讨论范围之内. 但是我们可以这样说:无论您使用什么类型的机制来注入bean, 总有办法为它编写JUnit测试.

在字段注入的情况下,代码是这样的:

@Component
公共类CalculateArea {

    @Autowired
    SquareService SquareService;
    @Autowired
    RectangleService RectangleService;
    @Autowired
    CircleService CircleService;

    public Double calculateArea(类型类型,Double... r )
    {
        //(与之前相同的实现)
    }
}

注意:因为我们使用的是字段注入, 不需要参数化的构造函数, 因此,使用默认对象创建对象,并使用字段注入机制设置值.

服务类的代码与上面相同, 但是测试类的代码如下:

公共类CalculateAreaTest {

    @Mock
    RectangleService RectangleService;
    @Mock
    SquareService SquareService;
    @Mock
    CircleService CircleService;

    @InjectMocks
    CalculateArea CalculateArea;

    @Before
    public void init()
    {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    公共void calculateRectangleAreaTest()
    {
        Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);
        Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d);
        Assert.assertequal(新双(20 d), calculatedArea);
    }
}

这里有一些不同之处:不是基本的,而是我们实现它的方式.

首先,我们模仿对象的方式:我们使用 @Mock annotations along with initMocks() to create mocks. 其次,将mock注入到实际对象中 @InjectMocks along with initMocks().

这样做只是为了减少代码行数.

什么是测试跑者,有哪些类型的跑者?

在上面的示例中,用于运行所有测试的基本运行器为 BlockJUnit4ClassRunner 哪一个检测所有注释并相应地运行所有测试.

如果我们想要更多的功能,那么我们可以编写一个自定义的运行程序. 例如,在上面的测试类中,如果我们想跳过一行 MockitoAnnotations.initMocks(this); 然后我们可以用另一种不同的跑步器 BlockJUnit4ClassRunner, e.g. MockitoJUnitRunner.

Using MockitoJUnitRunner,我们甚至不需要初始化mock并注入它们. That will be done by MockitoJUnitRunner 只是通过阅读注解.

(There’s also SpringJUnit4ClassRunner, which initializes the ApplicationContext 需要进行Spring集成测试——就像一个 ApplicationContext 在Spring应用程序启动时创建的. This we’ll cover later.)

Partial Mocking

当我们希望测试类中的对象模拟某些方法时, 还要调用一些实际的方法, 那我们就需要部分嘲讽. This is achieved via @Spy in JUnit.

Unlike using @Mock, with @Spy, a real object is created, 但是该对象的方法可以被模拟,也可以被实际调用——无论我们需要什么.

For example, if the area method in the class RectangleService calls an extra method log() 我们实际上想要打印这个日志,然后代码变成如下所示:

@Service
公共类RectangleService {

    公共双区(双r,双h)
    {
        log();
        return r*h;
    }

    public void log() {
        System.out.println("skip this");
    }
}

If we change the @Mock annotation of rectangleService to @Spy, 并做一些代码更改,如下所示,然后在结果中,我们会看到打印的日志, but the method area() will be mocked. That is, the original function is run solely for its side-effects; its return values are replaced by mocked ones.

@RunWith(MockitoJUnitRunner.class)
公共类CalculateAreaTest {

    @Spy
    RectangleService RectangleService;
    @Mock
    SquareService SquareService;
    @Mock
    CircleService CircleService;

    @InjectMocks
    CalculateArea CalculateArea;

    @Test
    公共void calculateRectangleAreaTest()
    {
        Mockito.doCallRealMethod().when(rectangleService).log();
        Mockito.when(rectangleService.area(5.0d,4.0d)).thenReturn(20d);

        Double calculatedArea = this.calculateArea.calculateArea(Type.RECTANGLE, 5.0d, 4.0d);
        Assert.assertequal(新双(20 d), calculatedArea);
    }
}

How Do We Go About Testing a Controller or RequestHandler?

从上面我们学到的 test code 在我们的例子中,控制器的属性如下所示:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
公共类arecontroller {

    @Autowired
    CalculateArea CalculateArea;

    @RequestMapping(value =“api/area”,method = RequestMethod.GET)
    @ResponseBody
    公共ResponseEntity calculateArea
        @RequestParam("type")字符串类型
        @RequestParam("param1")字符串param1,
        @RequestParam(value = "param2", required = false)字符串param2
    ) {
        try {
            Double area = calculateArea.calculateArea(
                Type.valueOf(type),
                Double.parseDouble(param1),
                Double.parseDouble(param2)
            );
            返回新的ResponseEntity(区域,HttpStatus.OK);
        }
        catch (Exception e)
        {
            return new ResponseEntity(e.getCause(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

@RunWith(MockitoJUnitRunner.class)

公共类areacontrolertest {

    @Mock
    CalculateArea CalculateArea;

    @InjectMocks
    AreaController AreaController;

    @Test
    公共void calculateAreaTest()
    {
        Mockito
        .when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d))
        .thenReturn(20d);

        ResponseEntity = arecontroller.calculateArea("RECTANGLE", "5", "4");
        Assert.assertEquals(HttpStatus.OK,responseEntity.getStatusCode());
        Assert.assertequal (20 d, responseEntity.getBody());
    }

}

看看上面的控制器测试代码, it works fine, 但它有一个基本问题:它只测试方法调用, not the actual API call. 所有那些需要针对不同输入测试API参数和API调用状态的测试用例都丢失了.

This code is better:

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

@RunWith (SpringJUnit4ClassRunner.class)

公共类areacontrolertest {

    @Mock
    CalculateArea CalculateArea;

    @InjectMocks
    AreaController AreaController;

    MockMvc mockMvc;

    @Before
    public void init()
    {
        mockMvc = standaloneSetup(arecontroller).build();
    }

    @Test
    公共void calculateAreaTest()抛出异常{
        Mockito
        .when(calculateArea.calculateArea(Type.RECTANGLE,5.0d, 4.0d))
        .thenReturn(20d);
        
        mockMvc.perform(
            MockMvcRequestBuilders.get("/api/area?type=RECTANGLE&param1=5&param2=4")
        )
        .andExpect(status().isOk())
        .andExpect(content().string("20.0"));
    }
}

Here we can see how MockMvc 执行实际的API调用. 它也有一些特殊的匹配器,比如 status() and content() 这使得验证内容变得容易.

使用JUnit和mock的Java集成测试

现在我们知道了代码的各个单元是如何工作的, 让我们进行一些Java集成测试,以确保这些单元按照预期相互交互.

First, 我们需要实例化所有bean, 与应用程序启动时Spring上下文初始化时发生的事情相同.

为此,我们在一个类中定义所有bean TestConfig.java:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class TestConfig {

    @Bean
    公共区域控制器区域控制器
    {
        return new AreaController();
    }
    @Bean
    public CalculateArea ()
    {
        return new CalculateArea();
    }

    @Bean
    RectangleService ()
    {
        返回新的RectangleService();
    }

    @Bean
    公共SquareService ()
    {
        return new SquareService();
    }

    @Bean
    CircleService ()
    {
        return new CircleService();
    }
}

现在让我们看看我们如何使用这个类并编写一个JUnit集成测试:

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

@RunWith (SpringJUnit4ClassRunner.class)
@ContextConfiguration(类= {TestConfig.class})

公共类areaconcontrollerintegrationtest {

    @Autowired
    AreaController AreaController;

    MockMvc mockMvc;

    @Before
    public void init()
    {
        mockMvc = standaloneSetup(arecontroller).build();
    }

    @Test
    公共void calculateAreaTest()抛出异常{
        mockMvc.perform(
            MockMvcRequestBuilders.get("/api/area?type=RECTANGLE&param1=5&param2=4")
        )
        .andExpect(status().isOk())
        .andExpect(content().string("20.0"));
    }
}

A few things change here:

  • @ContextConfiguration(类= {TestConfig.class})-这告诉测试用例所有bean定义驻留在哪里.
  • Now instead of @InjectMocks we use:
    @Autowired
    AreaController AreaController;

其他一切都保持不变. 如果我们调试测试,我们会看到代码实际上一直运行到 area() method in RectangleService where return r*h is calculated. 换句话说,实际的业务逻辑运行.

这并不意味着在集成测试中没有方法调用或数据库调用的模拟. In the above example, 没有使用第三方服务或数据库, 因此,我们不需要使用mock. 在现实生活中,这样的应用程序很少,我们经常会碰到数据库或第三方API,或者两者兼而有之. 在这种情况下,当我们在 TestConfig 类时,我们不创建实际对象,而是创建一个模拟对象,并在需要时使用它.

奖励:如何创建大型对象测试数据

通常,阻止后端开发人员编写单元或集成测试的是我们必须为每个测试准备的测试数据.

通常如果数据足够小, having one or two variables, 然后很容易创建测试数据类的对象并分配一些值.

For example, 如果我们期望一个模拟对象返回另一个对象, 当在模拟对象上调用函数时,我们会这样做:

Class1 object = new Class1();
object.setVariable1(1);
object.setVariable2(2);

然后为了使用这个物体,我们会这样做:

        Mockito.when(service.method(arguments...)).thenReturn(object);

这在上面的JUnit示例中很好,但是在上面的成员变量中 Class1 类不断增加,然后设置单个字段变得相当痛苦. 有时甚至可能发生一个类定义了另一个非原语类成员的情况. Then, 创建该类的对象并设置单个必需字段进一步增加了开发工作量,只是为了完成一些样板文件.

解决方案是生成上述类的JSON模式,并在JSON文件中添加相应的数据一次. 在测试类中,我们创建 Class1 对象,我们不需要手动创建对象. 相反,我们读取JSON文件并使用 ObjectMapper, map it into the required Class1 class:

ObjectMapper ObjectMapper = new ObjectMapper();
Class1 object = objectMapper.readValue(
    new String(Files.readAllBytes(
        Paths.get (" src /测试/资源/”+文件名))
    ),
    Class1.class
);

这是创建JSON文件并向其添加值的一次性工作. 之后的任何新测试都可以使用该JSON文件的副本,其中的字段会根据新测试的需要进行更改.

JUnit基础:多种方法和可转移技能

显然,编写Java单元测试的方法有很多种,这取决于我们如何选择注入bean. Unfortunately, 大多数关于这个话题的文章都倾向于假设只有一种方法, so it’s easy to get confused, 特别是在处理在不同假设下编写的代码时. Hopefully, 我们在这里的方法节省了开发人员找出模拟的正确方法和使用哪个测试运行器的时间.

无论我们使用什么语言或框架(甚至可能是任何新版本的Spring或JUnit),其概念基础都与上面JUnit教程中解释的相同. Happy testing!

关于总博客的进一步阅读:

Understanding the basics

  • 如何用Java编写单元测试?

    JUnit是用Java编写单元测试的最著名的框架. 使用Java中的JUnit测试,您可以编写调用要测试的实际方法的测试方法. 测试用例通过根据期望值断言返回值来验证代码的行为, given the parameters passed.

  • Java最好的单元测试框架是什么?

    大多数Java开发人员都认为JUnit是最好的单元测试框架. 自1997年以来,Java中的JUnit测试已经成为事实上的标准, 与其他Java单元测试框架相比,它当然拥有最多的支持. JUnit对于Java集成测试也很有用.

  • 什么是编程中的单元测试?

    In unit testing, individual units (often, 对象方法被认为是一个“单元”)以自动化的方式进行测试.

  • Why do we use JUnit testing?

    JUnit测试用于测试我们所编写的类中的方法的行为. 我们测试方法的预期结果,有时测试抛出异常的情况,即该方法是否能够以我们想要的方式处理异常.

  • What is the use of JUnit?

    JUnit是一个框架,它提供了许多不同的类和方法来轻松编写单元测试.

  • Is JUnit open source?

    是的,JUnit是一个开源项目,由许多活跃的开发人员维护.

  • Why is JUnit important?

    JUnit减少了开发人员在编写单元测试时需要使用的样板文件.

  • Who invented JUnit?

    Kent Beck和Erich Gamma最初创建了JUnit. 如今,这个开源项目有超过100个贡献者.

就这一主题咨询作者或专家.
Schedule a call
Nikhil Bansal的头像
Nikhil Bansal

Located in Gurgaon, Haryana, India

Member since November 27, 2018

About the author

Nikhil (BTech)已经自动化了从J2EE到Elasticsearch到Kafka的所有金融部门代码. 当然,所有的东西都是AWS.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Expedia Group

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.