如何更好的做单元测试并用它来提升代码质量(上)

简介: > 一个使用mockito和spring-test的例子 > 可以在:`https://github.com/weipeng2k/mockito-sample`找到示例。        Java单元测试框架在业界非常多,以JUnit为事实上的标准,而JUnit只是解决了单元测试的基本骨干,而对于Mock的支持却没有。

一个使用mockito和spring-test的例子

可以在:https://github.com/weipeng2k/mockito-sample找到示例。

       Java单元测试框架在业界非常多,以JUnit为事实上的标准,而JUnit只是解决了单元测试的基本骨干,而对于Mock的支持却没有。而同样,在Mock方面,Java也有很多开源的选择,诸如JMock、EasyMock和Mockito,而Mockito也同样为其中的翘楚,二者能够很好的完成单元测试的工作。本示例就是介绍如何使用二者来完成单元测试。如果公司自己搞一个单元测试框架,维护将成为一个大问题,而使用业界成熟的解决方案,将会是一个很好的方式。因为会有一组非常专业的人替你维护,而且不断地有新的Feature可以使用,同样你熟悉这些之后你可以不断的复用这些知识,而不会由于局限在某个特定的框架下(其实这些特定的框架也只是封装了业界的开源方案)。使用JUnit做单元测试的主体框架,如果有Spring的支持,可以使用spring-test进行支持,对于层与层之间的Mock,则使用Mockito来完成。

前言

引言

"I'm not a great programmer; I'm just a good programmer with great habits."
-- Kent Beck

       Java单元测试框架在业界非常多,以JUnit为事实上的标准,而JUnit只是解决了单元测试的基本骨干,而对于Mock的支持却没有。而同样,在Mock方面,Java也有很多开源的选择,诸如JMockEasyMockMockito,而Mockito也同样为其中的翘楚,二者能够很好的完成单元测试的工作。本文就是介绍如何使用二者来完成单元测试。

存在的问题

       如果公司自己搞一个单元测试框架,维护将成为一个大问题,而使用业界成熟的解决方案,将会是一个很好的方式。因为会有一组非常专业的人替你维护,而且不断地有新的Feature可以使用,同样你熟悉这些之后你可以不断的复用这些知识,而不会由于局限在某个特定的框架下(其实这些特定的框架也只是封装了业界的开源方案)。

解决方案

       使用JUnit做单元测试的主体框架,如果有Spring的支持,可以使用spring-test进行支持,对于层与层之间的Mock,则使用Mockito来完成。

使用Mockito进行单元测试

以下例子可以在mockito-test-case中找到。

使用Mockito进行mock

       先看一下怎样使用Mockito进行一个对象的Mock,首先添加依赖:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
</dependency>

       接下来尝试对java.util.List进行Mock,Mock对于List操作的内容进行构造。

构造Mock

       先看一下最简的使用方式。

public void mock_one() {
    List<String> list = Mockito.mock(List.class);

    Mockito.when(list.get(0)).thenReturn("one");

    System.out.println(list.get(0));

    Assert.assertEquals("one", list.get(0));
}

       上面代码中Mockito.mock可以构造一个Mock对象,这个对象没有任何作用,如果调用它的方法,如果有返回值的话,它会返回null。这个时候可以向其中加入mock逻辑,比如:Mockito.when(xxx.somemethod()).thenReturn(xxx),这段逻辑就会在当有外界调用xxx.somemethod()时,返回那个在thenReturn中的对象。

构造一个复杂的Mock

       有时我们需要针对输入来构造Mock的输出,简单的when和thenReturn无法支持,这时就需要较为复杂的Answer

@Test(expected = RuntimeException.class)
public void mock_answer() {
    List<String> list = Mockito.mock(List.class);
    Mockito.when(list.get(Mockito.anyInt())).thenAnswer(
            invocation -> {
                Object[] args = invocation.getArguments();
                int index = Integer.parseInt(args[0].toString());
                // int index = (int) args[0];
                if (index == 0) {
                    return "0";
                } else if (index == 1) {
                    return "1";
                } else if (index == 2) {
                    throw new RuntimeException();
                } else {
                    return String.valueOf(index);
                }
            });

    Assert.assertEquals("0", list.get(0));
    Assert.assertEquals("1", list.get(1));
    list.get(2);
}

       有时候需要构造复杂的返回逻辑,比如参数为1的时候,返回一个值,为2的时候,返回另一个值。那么when和thenAnswer就可以满足要求。

       上面代码可以看到当对于List的任意的输入Mockito.anyInt(),会进行Answer回调的处理,任何针对List的输入都会经过它的处理。这可以让我完成更加柔性和定制化的Mock操作。

断言选择

       当然我们可以使用System.out.println来完成目测,但是有时候需要让JUnit插件或者maven的surefire插件能够捕获住测试的失败,这个时候就需要使用断言了。我们使用org.junit.Assert来完成断言的判断,可以看到通过简单的assertEquals就可以了,当然该类提供了一系列的assertXxx来完成断言。

       使用IDEA在进行断言判断时非常简单,比Eclipse要好很多,比如:针对一个int x判断它等于0,就可以直接写x == 0,然后代码提示生成断言。

真实案例

       下面我们看一个较为真实的例子,比如:我们有个MemberService用来insertMember。

public interface MemberService {
    /**
     * <pre>
     * 插入一个会员,返回会员的主键
     * 如果有重复,则会抛出异常
     * </pre>
     *
     * @param name     name不能超过32个字符,不能为空
     * @param password password不能全部是数字,长度不能低于6,不超过16
     * @return PK
     */
    Long insertMember(String name, String password) throws IllegalArgumentException;
}

       其对应的实现。

public class MemberServiceImpl implements MemberService {

    private UserDAO userDAO;

    @Override
    public Long insertMember(String name, String password)
            throws IllegalArgumentException {
        if (name == null || password == null) {
            throw new IllegalArgumentException();
        }

        if (name.length() > 32 || password.length() < 6
                || password.length() > 16) {
            throw new IllegalArgumentException();
        }

        boolean pass = false;
        for (Character c : password.toCharArray()) {
            if (!Character.isDigit(c)) {
                pass = true;
                break;
            }
        }
        if (!pass) {
            throw new IllegalArgumentException();
        }

        Member member = userDAO.findMember(name);
        if (member != null) {
            throw new IllegalArgumentException("duplicate member.");
        }

        member = new Member();
        member.setName(name);
        member.setPassword(password);
        Long id = userDAO.insertMember(member);

        return id;
    }

    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

}

       可以看到实现通过聚合了userDAO,来完成操作,而业务层的代码的单元测试代码,就必须隔离UserDAO,也就是说要Mock这个UserDAO。

       下面我们就使用Mockito来完成Mock操作。

public class MemberWithoutSpringTest {
    private MemberService memberService = new MemberServiceImpl();

    @Before
    public void mockUserDAO() {
        UserDAO userDAO = Mockito.mock(UserDAO.class);
        Member member = new Member();
        member.setName("weipeng");
        member.setPassword("123456abcd");
        Mockito.when(userDAO.findMember("weipeng")).thenReturn(member);

        Mockito.when(userDAO.insertMember((Member) Mockito.any())).thenReturn(
                System.currentTimeMillis());

        ((MemberServiceImpl) memberService).setUserDAO(userDAO);
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_member_error() {
        memberService.insertMember(null, "123");

        memberService.insertMember(null, null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_exist_member() {
        memberService.insertMember("weipeng", "1234abc");
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_illegal_argument() {
        memberService
                .insertMember(
                        "akdjflajsdlfjaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfsadfasdfasf",
                        "abcdcsfa123");
    }

    @Test
    public void insert_member() {
        System.out.println(memberService.insertMember("windowsxp", "abc123"));
        Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
    }
}

       可以看到,在测试开始的时候,利用了Before来完成Mock对象的构建,也就是说在test执行之前完成了Mock对象的初始化工作。

       但仔细看上述代码中,MemberService的实现MemberServiceImpl是直接构造出来的,它依赖了实现,但是我们的测试最好不要依赖实现进行测试的。同时UserDAO也是硬塞给MemberService的实现,这是因为我们常用Spring来装配类之间的关系,而单元测试没有Spring的支持,这就使得测试代码需要硬编码的方式来进行组装。

       那么我们如何避免这样的强依赖和组装代码的出现呢?结论就是使用spring-test来完成。

使用Spring-Test来进行单元测试

以下例子可以在classic-spring-test中找到。

       spring-test是springframework中一个模块,主要也是由spring作者Juergen Hoeller来完成的,它可以方便的测试基于spring的代码。

引入spring-test

       spring-test只需要引入依赖就可以完成测试,非常简单。它能够帮助我们启动一个测试的spring容器,完成属性的装配,但是它如何同Mockito集成起来是一个问题,我们采用配置的方式进行。

加入依赖

       增加依赖:

该版本一般和你使用的spring版本一致

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <scope>test</scope>
</dependency>

配置

       由于Mockito支持mock方法构造,所以我们可以将它通过spring factory bean的形式融入到 spring 的体系中。我们针对MemberService进行测试,需要对UserDAO进行Mock,我们只需要在配置中配置即可。

配置在MemberService.xml中,这里需要说明一下 没有使用共用的配置文件, 目的就是让大家在测试的时候能够相互独立,而且在一个配置文件中配置的Bean越多,就证明你要测试的类依赖越复杂,也就是越不合理,逼迫自己做重构

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
       default-autowire="byName">

    <bean id="memberService" class="com.murdock.tools.mockito.service.MemberServiceImpl"/>

    <bean id="userDAO" class="org.mockito.Mockito" factory-method="mock">
        <constructor-arg>
            <value>com.murdock.tools.mockito.dao.UserDAO</value>
        </constructor-arg>
    </bean>
</beans>

       在进行spring测试之前,我们必须有一个spring的配置文件,用来构造applicationContext,注意上面红色的部分,这个UserDAO就是MemberServiceImpl需要的,而它利用了spring的FactoryBean方式,通过mock工厂方法完成了Mock对象的构造,其中的构造函数表明了这个Mock是什么类型的。只用在配置文件中声明一下就可以了。

构造Mock

       先看一下使用spring-test如何写单元测试:

@ContextConfiguration(locations = {"classpath:MemberService.xml"})
public class MemberSpringTest extends AbstractJUnit4SpringContextTests {
    @Autowired
    private MemberService memberService;
    @Autowired
    private UserDAO userDAO;

    /**
     * 可以选择在测试开始的时候来进行mock的逻辑编写
     */
    @Before
    public void mockUserDAO() {
        Mockito.when(userDAO.insertMember(Mockito.any())).thenReturn(
                System.currentTimeMillis());
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_member_error() {
        memberService.insertMember(null, "123");

        memberService.insertMember(null, null);
    }

    /**
     * 也可以选择在方法中进行mock
     */
    @Test(expected = IllegalArgumentException.class)
    public void insert_exist_member() {
        Member member = new Member();
        member.setName("weipeng");
        member.setPassword("123456abcd");
        Mockito.when(userDAO.findMember("weipeng")).thenReturn(member);

        memberService.insertMember("weipeng", "1234abc");
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_illegal_argument() {
        StringBuilder sb = new StringBuilder();
        IntStream.range(0, 32).forEach(sb::append);
        
        memberService.insertMember(sb.toString(), "abcdcsfa123");
    }

    @Test
    public void insert_member() {
        System.out.println(memberService.insertMember("windowsxp", "abc123"));
        Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
    }
}

       可以看到,通过继承AbstractJUnit4SpringContextTests就可以完成构造applicationContext的功能。当然通过ContextConfiguration指明当前的配置文件所在地,就可以完成applicationContext的初始化,同时利用Autowired完成配置文件中的Bean的获取。

       由于在MemberService.xml中针对UserDAO的mock配置,对应的mock对象会被注入到MemberSpringTest中,而后续的测试方法就可以针对它来编排mock逻辑。

       我们在Before逻辑中以及方法中均可以自由的裁剪mock逻辑,这样JUnitspring-testMockito完美的统一到了一起。

目录
相关文章
|
23天前
|
敏捷开发 安全 测试技术
掌握单元测试:确保代码质量的关键步骤
单元测试是确保代码质量、提升可维护性和可靠性的重要手段。本文介绍了单元测试的基本概念、重要性及最佳实践,包括测试驱动开发(TDD)、保持测试独立性、使用断言库和模拟依赖等,旨在帮助开发者掌握单元测试技巧,提高开发效率。
|
1月前
|
前端开发 JavaScript 测试技术
前端小白逆袭之路:如何快速掌握前端测试技术,确保代码质量无忧!
【10月更文挑战第30天】前端开发技术迭代迅速,新手如何快速掌握前端测试以确保代码质量?本文将介绍前端测试的基础知识,包括单元测试、集成测试和端到端测试,以及常用的测试工具如Jest、Mocha、Cypress等。通过实践和学习,你也能成为前端测试高手。
45 4
|
2月前
|
设计模式 关系型数据库 测试技术
进阶技巧:提高单元测试覆盖率与代码质量
【10月更文挑战第14天】随着软件复杂性的不断增加,确保代码质量的重要性日益凸显。单元测试作为软件开发过程中的一个重要环节,对于提高代码质量、减少bug以及加快开发速度都有着不可替代的作用。本文将探讨如何优化单元测试以达到更高的测试覆盖率,并确保代码质量。我们将从编写有效的测试用例策略入手,讨论如何避免常见的测试陷阱,使用mocking工具模拟依赖项,以及如何重构难以测试的代码。
67 4
|
3月前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
90 5
|
4月前
|
IDE Java 测试技术
揭秘Java高效编程:测试与调试实战策略,让你代码质量飞跃,职场竞争力飙升!
【8月更文挑战第30天】在软件开发中,测试与调试对确保代码质量至关重要。本文通过对比单元测试、集成测试、调试技巧及静态代码分析,探讨了多种实用的Java测试与调试策略。JUnit和Mockito分别用于单元测试与集成测试,有助于提前发现错误并提高代码可维护性;Eclipse和IntelliJ IDEA内置调试器则能快速定位问题;Checkstyle和PMD等工具则通过静态代码分析发现潜在问题。综合运用这些策略,可显著提升代码质量,为项目成功打下坚实基础。
64 2
|
4月前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
随着软件行业的发展,代码质量和效率变得至关重要。自动化测试与单元测试是保证质量、提升效率的关键。Python凭借其简洁强大及丰富的测试框架(如Selenium、Appium、pytest和unittest等),成为了实施自动化测试的理想选择。本文将深入探讨这些框架的应用,帮助读者掌握编写高质量测试用例的方法,并通过持续集成等策略提升开发流程的效率与质量。
52 4
|
4月前
|
监控 jenkins 测试技术
自动化测试中的“守护神”: 持续集成与代码质量监控
【8月更文挑战第31天】在软件开发的海洋里,自动化测试犹如一座灯塔,指引着项目向着高质量和高效率的方向前进。本文将深入探讨如何通过持续集成(CI)和代码质量监控相结合的方式,构建起一道坚固的防线,保障软件项目在快速迭代中不失方向。我们将一起探索这一过程中的关键实践,以及它们是如何相互作用,共同提升软件项目的可靠性和稳定性。
|
5月前
|
测试技术 数据库 开发者
开发与运维测试问题之高代码覆盖率意味着高代码质量如何解决
开发与运维测试问题之高代码覆盖率意味着高代码质量如何解决
|
5月前
|
jenkins 数据管理 测试技术
在LabVIEW开发生命周期中提高代码质量的自动化测试方法
在LabVIEW开发生命周期中提高代码质量的自动化测试方法
60 0
|
7月前
|
Java 测试技术 Android开发
Java 测试和调试:提高代码质量的实用策略
【4月更文挑战第27天】测试和调试是软件开发中确保应用稳定、高效且可靠的关键步骤。对于 Java 开发者来说,掌握有效的测试和调试技巧可以大大提高代码质量和减少生产环境下的问题。
79 2