应用程序在分发之前应该经过测试和验证。测试的目的是验证应用程序是否符合功能和非功能要求,并检测应用程序中的错误。
TDD:测试驱动开发
一旦需求和规范得到验证,就可以开始一个称为测试驱动开发的过程。您首先编写测试,然后开发代码。
将根据商定的要求和规范创建测试(测试评审方案);最初测试会失败,我们将在应用程序中编写代码以确保测试通过。一旦测试通过,我们可以重构应用程序中的代码以改进它并再次启动测试。
此类测试应由分析师设计并由开发人员实施。如果我们注意到某个规范的测试很难开发,我们应该考虑这样一个事实,即该规范可能不正确或至少是不明确的。
多亏了 TDD 技术,我们可以在开发的早期阶段发现任何问题。考虑到解决问题的努力与找到问题所需的时间成正比。
单元和集成测试
单元测试作为类方法验证应用程序的一小部分的功能,并且独立于应用程序的其他单元并隔离。
好的公司,单元测试都是在开发完毕后,自己就进行处理完成了。当然测试人员不是不可以做,主要是侧重点不同。
我们可以将各个单元视为应用程序的各个层。
因此,一个好的单元测试独立于整个应用程序基础设施,如数据库类型和其他层。如果测试方法与其他单元有依赖关系,它们可以被模拟(可能使用像 Mockito 这样的库)。在我们将要做的示例中,我们将测试一个 Service 方法,并且该测试将独立于数据库和 Spring 的上下文(因此该测试适用于任何用于依赖注入的框架)。
集成测试验证应用程序的多个单元的操作。这里使用的框架的上下文也用于测试阶段。
需求示例
根据需求,我们被要求实现 findById 的 REST API 并创建一个 User 模型。要求产品经理提供明确的需求,开发去实现产品提供的需求。
成功返回:
特别是,在 findById 中,客户端必须收到 200 并且在响应正文中收到找到的用户的 JSON。
失败或者不存在返回:
如果没有具有输入 id 的用户,则客户端必须收到带有空正文的 404。
此外,客户端会看到用户的 2 个字段:姓名和地址;名称由姓氏、空格和名字组成。
该项目将具有以下层:
实体将包含具有姓名、姓氏和地址的实体用户
dtos 将包含映射实体 User 的 DTO UserDTO,带有字段名称(“姓氏 + 名字”)和地址
负责将 User 转换为 UserDTO 的转换器,反之亦然
存储库将包含实体用户的 Spring JpaRepository
将包含 UserService 的服务,该服务将从数据库中轻松检索用户并使用转换器将其转换为 DTO
将包含 UserController 的控制器,它负责映射 REST 调用并使用服务的业务逻辑。
第一步:让我们创建实体和 dto
@Entity
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String surname;
private String address;
public User() {}
public User(String name, String surname, String address) {
this.name = name;
this.surname = surname;
this.address = address;
}
//getter, setter, equals and hashcode
}
@Entity
public class UserDTO {
//surname + name
private String name;
private String address;
public UserDTO() {}
public UserDTO(String name, String address) {
this.name = name;
this.address = address;
}
//getter, setter, equals and hashcode
}
第二步:让我们创建转换器
@Component
public class UserConverter {
public UserDTO userToUserDTO(User user) {
return new UserDTO(user.getSurname() + " " + user.getName(), user.getAddress());
}
public User userDTOToUser(UserDTO userDTO) {
String[] surnameAndName = userDTO.getName().split(" ");
return new User(surnameAndName[1], surnameAndName[0], userDTO.getAddress());
}
}
第三步:让我们为 User 实体创建一个存储库
public interface UserRepository extends JpaRepository
}
第四步:让我们用 findById 方法创建服务
@Service
public class UserServiceImpl implements UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
private UserRepository userRepository;
private UserConverter userConverter;
public UserServiceImpl(UserRepository userRepository, UserConverter userConverter) {
this.userRepository = userRepository;
this.userConverter = userConverter;
}
@Override
public UserDTO findById(Long id) {
return null;
}
}
正如我们所见,该方法目前返回 null。我们将在运行测试后实现该功能。
第五步:我们创建测试类来测试服务的findById方法
当输入中提供有效值时,甚至当提供无效值时,良好的测试应该验证方法的行为。
该服务依赖于存储库和转换器。我们对存储库的实现不感兴趣,因此模拟它是一个好主意。对于转换器,我们可以考虑做同样的事情,但由于它是一个非常琐碎的类,我们可以考虑在测试中使用真正的类,但不带出Spring的上下文。
建立一个测试类
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@Spy
private UserConverter userConverter;
private UserService userService;
@BeforeEach
public void init() {
userService = new UserServiceImpl(userRepository, userConverter);
}
@Test
public void findByIdSuccess() {
User user = new User("Vincenzo", "Racca", "via Roma");
user.setId(1L);
when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));
UserDTO userDTO = userService.findById(1L);
verify(userRepository, times(1)).findById(anyLong());
assertNotNull(userDTO);
String[] surnameAndName = userDTO.getName().split( " ");
assertEquals(2, surnameAndName.length);
assertEquals(user.getSurname(), surnameAndName[0]);
assertEquals(user.getName(), surnameAndName[1]);
assertEquals(user.getAddress(), userDTO.getAddress());
}
}
我们来分析一下代码:
@ExtendWith(MockitoExtension.class) 允许我们使用 Mockito 库的 mockato 上下文。 @ExtendWith 也对应于 JUnit 4 的 @RunWith。
我们使用 @Mock 注释存储库,因为我们希望 Mockito 创建接口的 mockata 实现。
我们使用@Spy 对转换器进行注释,以向 Mockito 表明我们想要使用真正的类。
我们使用 @BeforeEach 注释每次运行测试方法时初始化 userService 的 init 方法。由于我们使用了构造函数依赖注入而不是字段注入,我们只需要在构造函数中传入 mockato 存储库即可创建服务。 @BeforeEach 对应于 JUnit 4 的 @Before 注解。
当然这里也可用junit5或者是testng
我们来分析一下测试:
我们期望对于任何输入 id,存储库将返回一个名为 Vincenzo、姓氏 Racca 和通过 Roma 的地址的用户。为此,我们将 Mockito 的静态方法与 anyLong(指示任何 id)一起使用,然后使用 thenReturn 指示我们期望从该方法中获得的返回值。当我们调用该服务时,我们期望返回一个名为 Racca Vincenzo 和地址 via Roma.\ 的 UserDTO。通过验证,我们验证服务调用存储库的 findById 1 次。然后遵循各种琐碎的断言。
我们执行测试:它在调用 verify 时已经失败,因为服务方法从未调用存储库;
第六步:让我们修复方法
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
super("User with id " + id + " not found!");
}
}
@Test
public void findByIdUnSuccess() {
when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
UserNotFoundException exp = assertThrows(UserNotFoundException.class, () -> userService.findById(1L));
assertEquals("User with id 1 not found!", exp.getMessage());
}
我们非常简单地告诉 Mockito,对于任何 id,存储库都不会找到任何用户。使用 assertThrows 我们返回我们期望由服务启动的异常,然后我们在异常消息上写一个断言。这种测试在 JUnit 5 中是可能的。实际上,在 JUnit 4 中,我们可以验证该方法是否启动了异常,但是一旦启动了错误,您就无法继续进行断言。
显然测试失败,所以让我们修复方法。
现在测试通过了,我们可以评估以改进代码而不会忘记重新测试该方法。
第七步:重构方法
@Override
public UserDTO findById(Long id) {
User user = userRepository.findById(id).orElseThrow(() -> {
UserNotFoundException exp = new UserNotFoundException(id);
LOGGER.error("Exception is UserServiceImpl.findById", exp);
return exp;
});
return userConverter.userToUserDTO(user);
}
结论
我们通过使用 JUnit 5 开发单元测试并在 Mockito 的帮助下模拟测试不感兴趣的单元,简要了解了测试驱动开发的工作原理。在下一篇文章中,我们将通过创建控制器来继续开发需求,我们将使用 Spring Boot、JUnit 5 和 H2 作为内存数据库编写集成测试。