作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Nikhil (BTech)已经自动化了从J2EE到Elasticsearch到Kafka的所有金融部门代码. 当然,所有的东西都是AWS.
在一个持续交付的时代, Java开发人员必须确信他们的更改不会破坏现有代码, hence automated testing. 有不止一种有效的方法,但你如何才能保持它们的正确性?
随着技术和行业的进步,从瀑布模型转向敏捷,现在又转向DevOps, 应用程序中的更改和增强在完成后立即部署到生产环境中. 代码部署到生产环境的速度如此之快, 我们需要确信我们的改变是有效的, 而且它们不会破坏任何先前存在的功能.
要建立这种信心,我们必须有 framework 用于自动回归测试. 进行回归测试, 从api级别的角度来看,应该执行许多测试, 但在这里,我们将介绍两种主要类型的测试:
每种编程语言都有许多可用的框架. 我们将专注于编写单元和集成测试的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.)
当我们希望测试类中的对象模拟某些方法时, 还要调用一些实际的方法, 那我们就需要部分嘲讽. 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);
}
}
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¶m1=5¶m2=4")
)
.andExpect(status().isOk())
.andExpect(content().string("20.0"));
}
}
Here we can see how MockMvc
执行实际的API调用. 它也有一些特殊的匹配器,比如 status()
and content()
这使得验证内容变得容易.
现在我们知道了代码的各个单元是如何工作的, 让我们进行一些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¶m1=5¶m2=4")
)
.andExpect(status().isOk())
.andExpect(content().string("20.0"));
}
}
A few things change here:
@ContextConfiguration(类= {TestConfig.class})
-这告诉测试用例所有bean定义驻留在哪里.@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文件的副本,其中的字段会根据新测试的需要进行更改.
显然,编写Java单元测试的方法有很多种,这取决于我们如何选择注入bean. Unfortunately, 大多数关于这个话题的文章都倾向于假设只有一种方法, so it’s easy to get confused, 特别是在处理在不同假设下编写的代码时. Hopefully, 我们在这里的方法节省了开发人员找出模拟的正确方法和使用哪个测试运行器的时间.
无论我们使用什么语言或框架(甚至可能是任何新版本的Spring或JUnit),其概念基础都与上面JUnit教程中解释的相同. Happy testing!
JUnit是用Java编写单元测试的最著名的框架. 使用Java中的JUnit测试,您可以编写调用要测试的实际方法的测试方法. 测试用例通过根据期望值断言返回值来验证代码的行为, given the parameters passed.
大多数Java开发人员都认为JUnit是最好的单元测试框架. 自1997年以来,Java中的JUnit测试已经成为事实上的标准, 与其他Java单元测试框架相比,它当然拥有最多的支持. JUnit对于Java集成测试也很有用.
In unit testing, individual units (often, 对象方法被认为是一个“单元”)以自动化的方式进行测试.
JUnit测试用于测试我们所编写的类中的方法的行为. 我们测试方法的预期结果,有时测试抛出异常的情况,即该方法是否能够以我们想要的方式处理异常.
JUnit是一个框架,它提供了许多不同的类和方法来轻松编写单元测试.
是的,JUnit是一个开源项目,由许多活跃的开发人员维护.
JUnit减少了开发人员在编写单元测试时需要使用的样板文件.
Kent Beck和Erich Gamma最初创建了JUnit. 如今,这个开源项目有超过100个贡献者.
Located in Gurgaon, Haryana, India
Member since November 27, 2018
Nikhil (BTech)已经自动化了从J2EE到Elasticsearch到Kafka的所有金融部门代码. 当然,所有的东西都是AWS.
世界级的文章,每周发一次.
世界级的文章,每周发一次.
Join the Toptal® community.