stringboot TDD测试驱动开发

应用程序在分发之前应该经过测试和验证。测试的目的是验证应用程序是否符合功能和非功能要求,并检测应用程序中的错误。

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 作为内存数据库编写集成测试。

你可能感兴趣的:(stringboot TDD测试驱动开发)