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<User,Long> {

}

第四步:让我们用 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 作为内存数据库编写集成测试。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容