在单元测试(JUnit 5)中是否需要断言模拟对象和方法?
Is asserting mocked object and method necessary in Unit Test(JUnit 5)?
我仍然是单元测试的基础学习者。
我试图在不直接调用它的情况下模拟我的 class 方法(因为它依赖于第 3 方库)并且我编写了如下所示的测试方法。
(我提到的第 3 方库不是 MapStruct
,它是 ModelObject.class
,它的构造函数参数非常复杂,只能在库级别初始化)
ProjectMapper
class的toDto
方法是一个简单的对象映射方法(带有MapStruct
库)。
@Test
@DisplayName("Should convert Project ModelObject to Project DTO successfully.")
void testProjectModelObjectToDtoSucess() {
// Given
ModelObject mockModelObject = mock(ModelObject.class); // <-- Mocking "ModelObject.class"
ProjectDto expected = new ProjectDto("PPJT-00000001", "Test Project 01"); // <-- Initializing expected return object.
when(mockProjectMapper.toDto(mockModelObject)).thenReturn(expected); // <-- Mocking cut(class under test)'s method.
// When
ProjectDto actual = mockProjectMapper.toDto(mockModelObject); // <-- Calling mocked method.
// Then
assertEquals(actual.getId(), expected.getId(), "The converted DTO's ID should equal to expected DTO's ID.");
assertEquals(actual.getName(), expected.getName(), "The converted DTO's name should equal to expected DTO's name.");
}
我想知道的是,如果我已经假设 mockProjectMapper.toDto()
将 return 正好等于 expected
对象,为什么我需要用 actual
(returned) 对象?
我了解到单元测试应该测试任何可破解的代码。
我怀疑这种测试方式有什么好处,如果不合适,用 mocking 测试这种方法的正确方法是什么?
我参考了this example code的这个测试方法。
@refael-sheinker 这是 ProjectMapper
class 的源代码,它是 MapStruct
的界面 class.
@Mapper
public interface ProjectMapper {
ProjectMapper INSTANCE = Mappers.getMapper(ProjectMapper.class);
@Mapping(target = "id", source = "projectId")
@Mapping(target = "name", source = "projectName")
ProjectDto toDto(ModelObject modelObject);
}
您模拟了被测对象并存根了被测方法 - 您正确地认为这样的测试不会带来任何好处。
您正在检查您是否可以存根方法,而不是您的生产代码。
主要问题是您假设需要模拟每个第 3 方 class。这是错误的思维方式 - 你应该尝试模拟你的 class 的合作者(尤其是那些导致 side-effects 在测试环境中不需要的),而不是 classes 是关键部分class 的执行情况。该行有时可能会模糊,但在这个具体示例中,我强烈建议不要模拟映射器:
- 您的第 3 方 class 由注释驱动
- 注释必须与您的 DTO 字段相对应
- 注释中的字段名称是字符串,它们的存在是编译器不强制执行的 DTO
- 所有映射都在内存中完成
创建一个运行生产代码的测试让您有信心:
- 字段名称正确
- 相应的映射字段具有正确的类型(例如,您没有将 int 映射到 boolean)
服务的链接测试有些不同:它测试调用存储库的 Spring 服务。存储库可能代表一些与外界交互的数据库——它是一个很好的模拟对象。在我看来,服务方法只将调用转发到存储库——这是一个微不足道的行为——所以测试是微不足道的,但这个设置将扩展到更复杂的服务方法。
更新
MapStruct 的模拟源 class。
如果 ModelObject
创建起来很复杂,请考虑对其某些构造函数参数使用模拟。
var pN = mock(ParamN.class);
// possibly stub some interactions with pN
var modelObject = new ModelObject(10, 20, pN);
或者,您甚至可以模拟整个 ModelObect 并将其 getter 存根:
var mockModelObject = mock(ModelObject.class);
when(mockModelObject.getId()).thenReturn(10);
我仍然是单元测试的基础学习者。
我试图在不直接调用它的情况下模拟我的 class 方法(因为它依赖于第 3 方库)并且我编写了如下所示的测试方法。
(我提到的第 3 方库不是 MapStruct
,它是 ModelObject.class
,它的构造函数参数非常复杂,只能在库级别初始化)
ProjectMapper
class的toDto
方法是一个简单的对象映射方法(带有MapStruct
库)。
@Test
@DisplayName("Should convert Project ModelObject to Project DTO successfully.")
void testProjectModelObjectToDtoSucess() {
// Given
ModelObject mockModelObject = mock(ModelObject.class); // <-- Mocking "ModelObject.class"
ProjectDto expected = new ProjectDto("PPJT-00000001", "Test Project 01"); // <-- Initializing expected return object.
when(mockProjectMapper.toDto(mockModelObject)).thenReturn(expected); // <-- Mocking cut(class under test)'s method.
// When
ProjectDto actual = mockProjectMapper.toDto(mockModelObject); // <-- Calling mocked method.
// Then
assertEquals(actual.getId(), expected.getId(), "The converted DTO's ID should equal to expected DTO's ID.");
assertEquals(actual.getName(), expected.getName(), "The converted DTO's name should equal to expected DTO's name.");
}
我想知道的是,如果我已经假设 mockProjectMapper.toDto()
将 return 正好等于 expected
对象,为什么我需要用 actual
(returned) 对象?
我了解到单元测试应该测试任何可破解的代码。
我怀疑这种测试方式有什么好处,如果不合适,用 mocking 测试这种方法的正确方法是什么?
我参考了this example code的这个测试方法。
@refael-sheinker 这是 ProjectMapper
class 的源代码,它是 MapStruct
的界面 class.
@Mapper
public interface ProjectMapper {
ProjectMapper INSTANCE = Mappers.getMapper(ProjectMapper.class);
@Mapping(target = "id", source = "projectId")
@Mapping(target = "name", source = "projectName")
ProjectDto toDto(ModelObject modelObject);
}
您模拟了被测对象并存根了被测方法 - 您正确地认为这样的测试不会带来任何好处。
您正在检查您是否可以存根方法,而不是您的生产代码。
主要问题是您假设需要模拟每个第 3 方 class。这是错误的思维方式 - 你应该尝试模拟你的 class 的合作者(尤其是那些导致 side-effects 在测试环境中不需要的),而不是 classes 是关键部分class 的执行情况。该行有时可能会模糊,但在这个具体示例中,我强烈建议不要模拟映射器:
- 您的第 3 方 class 由注释驱动
- 注释必须与您的 DTO 字段相对应
- 注释中的字段名称是字符串,它们的存在是编译器不强制执行的 DTO
- 所有映射都在内存中完成
创建一个运行生产代码的测试让您有信心:
- 字段名称正确
- 相应的映射字段具有正确的类型(例如,您没有将 int 映射到 boolean)
服务的链接测试有些不同:它测试调用存储库的 Spring 服务。存储库可能代表一些与外界交互的数据库——它是一个很好的模拟对象。在我看来,服务方法只将调用转发到存储库——这是一个微不足道的行为——所以测试是微不足道的,但这个设置将扩展到更复杂的服务方法。
更新
MapStruct 的模拟源 class。
如果 ModelObject
创建起来很复杂,请考虑对其某些构造函数参数使用模拟。
var pN = mock(ParamN.class);
// possibly stub some interactions with pN
var modelObject = new ModelObject(10, 20, pN);
或者,您甚至可以模拟整个 ModelObect 并将其 getter 存根:
var mockModelObject = mock(ModelObject.class);
when(mockModelObject.getId()).thenReturn(10);