吃透单元测试:Spock单元测试框架的应用与实践

这篇具有很好参考价值的文章主要介绍了吃透单元测试:Spock单元测试框架的应用与实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一,单元测试

单元测试是对软件基本组成单元进行的测试,如函数或一个类的方法。程序是由函数组成的,每个函数都要健壮,这样才能保证程序的整体质量。单元测试是对软件未来的一项必不可少的投资。”具体来说,单元测试有哪些收益呢?

  • 它是最容易保证代码覆盖率达到100%的测试。
  • 可以⼤幅降低上线时的紧张指数。
  • 单元测试能更快地发现问题。
  • 单元测试的性价比最高,因为错误发现的越晚,修复它的成本就越高,而且难度呈指数式增长,所以我们要尽早地进行测试
  • 编码人员,一般也是单元测试的主要执行者,是唯一能够做到生产出无缺陷程序的人,其他任何人都无法做到这一点。
  • 有助于源码的优化,使之更加规范,快速反馈,可以放心进行重构。

尽管单元测试有如此的收益,但在我们日常的工作中,仍然存在不少项目它们的单元测试要么是不完整要么是缺失的。

1,为什么人人都讨厌写单测

要额外写很多很多的代码

一个高覆盖率的单测代码,往往比你要测试的,真正开发的业务代码要多,甚至是业务代码的好几倍。这让人觉得难以接受,你想想开发 5 分钟,单测 2 小时是什么样的心情。而且并不是单测写完就没事了,后面业务要是变更了,你所写的单测代码也要同步维护。

时间成本

代码逻辑过于复杂,写单元测试时耗费的时间较长,任务重、工期紧,写一个单测的时间可以实现一个需求,那么你如何去选?显而易见。

写单测是一件很无趣的事情

因为他比较死,主要目的就是为了验证,相比之下他更像是个体力活,没有真正写业务代码那种创造的成就感。写出来,验证不出bug很失落,白写了,验证出bug又感到自己是在打自己脸。

2,为什么又必须写单测

所以得到的结论就是不写单测?那么问题又来了,出来混迟早是要还的,上线出了问题,最终责任人是谁?不是提需求的产品、不是没发现问题的测试同学,他们顶多就是连带责任。最该负责的肯定是写这段代码的你。

所以单元测试保护的不仅仅是程序,更保护的是写程序的你。 最后得出了一个无可奈何的结论,单测是个让人又爱又恨的东西,是不想做但又不得不做的事情。

虽然我们没办法改变要写单测这件事,但是我们可以改变怎么去写单元测试这件事。

既然我们不得不去写单元测试,那么今天就为大家推荐一款比较神奇的单元测试框架,Spock去提高你编写单测的效率。

二,Spock是什么

spock官网:https://spockframework.org/spock/docs/2.0/index.html

Spock是国外一款优秀的测试框架,基于BDD(行为驱动开发)思想实现,功能非常强大。Spock结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。

那么spock 是如何提高编写单测的效率呢?

  1. 它可以用更少的代码去实现单元测试,让你可以更加专注于去验证结果而不是写单测代码的过程。(Spock使用groovy动态语言的特点)
  2. **他有更好的语义化,让你的单测代码可读性更高。**Spock提供多种语义标签,如: given、when、then、expect、where、with、and 等,从行为上规范单测代码,每一种标签对应一种语义,让我们的单测代码结构具有层次感,功能模块划分清晰,便于后期维护。

三,Spock使用

Spock引入

				<!--引入 groovy 依赖-->
				<dependency>
            <groupId>org.codehaus.groovy</groupId>
            <artifactId>groovy-all</artifactId>
            <version>2.4.15</version>
            <scope>test</scope>
        </dependency>
				<!--引入spock 与 spring 集成包-->
        <dependency>
            <groupId>org.spockframework</groupId>
            <artifactId>spock-spring</artifactId>
            <version>1.2-groovy-2.4</version>
            <scope>test</scope>
        </dependency>

Spock自带Mock功能,所以我们可以来Mock非静态方法。但是遇到静态方法时,我们需要导入powermock

				<!--powermock -->
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <version>2.0.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <version>2.0.0</version>
            <scope>test</scope>
        </dependency>

但是当我们需要测试dao层的sql语句时,我们可以结合H2内存数据库使用,此时需要引入:

				<!--db unit-->
        <dependency>
            <groupId>com.github.janbols</groupId>
            <artifactId>spock-dbunit</artifactId>
            <version>0.4</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.dbunit</groupId>
            <artifactId>dbunit</artifactId>
            <version>2.5.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.200</version>
            <scope>test</scope>
        </dependency>

以上就能满足我们平时用Spock来写单测的日常功能。

下面通过一个简单的例子,来体验一下Spock和junit的不同:
public class IDNumberUtils {

    /**
     * 根据身份证号码获取出生日期和年龄
     *
     * @param idNo 18位身份证号码
     * @return 返回格式: birth: 1992-01-01  age: 29
     */
    public static Map<String, String> getBirthAge(String idNo) {

        String birthday = "";
        String age = "";

        int year = Calendar.getInstance().get(Calendar.YEAR);
        birthday = idNo.substring(6, 10) + "-" + idNo.substring(10, 12) + "-" + idNo.substring(12, 14);
        age = String.valueOf(year - Integer.valueOf(idNo.substring(6, 10)));

        Map<String, String> result = new HashMap<>();
        result.put("birthday", birthday);
        result.put("age",age);

        return result;
    }
}

Junit单测:

public class IDNumberUtilsJunitTest {

    @ParameterizedTest
    @MethodSource("getBirthAgeParams")
    public void testGetBirthAge(String idNo,Predicate<Map<String,String>> predicate){
        Map<String, String> birthAgeMap = IDNumberUtils.getBirthAge(idNo);
        Assertions.assertTrue(predicate.test(birthAgeMap));
    }

    public static Object[] getBirthAgeParams(){
        return new Object[]{
                new Object[]{
                        "410225199208091234",(Predicate<Map<String,String>>) map -> "{birthday=1992-08-09, age=29}".equals(map.toString())
                },
                new Object[]{
                        "410225199308091234",(Predicate<Map<String,String>>) map -> "{birthday=1993-08-09, age=28}".equals(map.toString())
                },
                new Object[]{
                        "410225199408091234",(Predicate<Map<String,String>>) map -> "{birthday=1994-08-09, age=27}".equals(map.toString())
                },
                new Object[]{
                        "410225199508091234",(Predicate<Map<String,String>>) map -> "{birthday=1995-08-09, age=26}".equals(map.toString())
                },
                new Object[]{
                        "410225199608091234",(Predicate<Map<String,String>>) map -> "{birthday=1996-08-09, age=25}".equals(map.toString())
                },
        };
    }
}

Spock单测:

class IDNumberUtilsGroovyTest extends Specification {

    @Unroll
    def "身份证号:#idNO 的生日,年龄是:#result"() {
        expect: "执行以及结果验证"
        IDNumberUtils.getBirthAge(idNO) == result
        where: "测试用例覆盖"
        idNO                 || result
        "410225199208091234" || ["birthday": "1992-08-09", "age": "29"]
        "410225199308091234" || ["birthday": "1993-08-09", "age": "28"]
        "410225199408091234" || ["birthday": "1994-08-09", "age": "27"]
        "410225199508091234" || ["birthday": "1995-08-09", "age": "26"]
        "410225199608091234" || ["birthday": "1996-08-09", "age": "25"]
    }
}
  • def 是 groovy 的关键字,可以用来定义变量跟方法名。
  • 后面的是你的单元测试名称,可以用中文也可以用英文。
  • expect … where …语句块,expect 为核心的测试校验语句块。where 为多个测试用例的列举。

以上测试方法的语义为:列举多个where下面的多个测试用例,以idNo传参,result为结果,调用getBirthAge方法,来验证每条测试用例是否符合我们的预期,添加@Unroll注解,主要是让每一条测试用例都返回测试结果。

如果不加@Unroll注解,那么会把所有的测试用例返回一个结果。

吃透单元测试:Spock单元测试框架的应用与实践

四,Mock

我们的服务大部分是分布式微服务架构。服务与服务之间通常都是通过接口的方式进行交互。即使在同一个服务内也会分为多个模块,业务功能需要依赖下游接口的返回数据,才能继续后面的处理流程。这里的下游不限于接口,还包括中间件数据存储等等,所以如果想要测试自己的代码逻辑,就必须把这些依赖项Mock掉。因为如果下游接口不稳定可能会影响我们代码的测试结果,让下游接口返回指定的结果集(事先准备好的数据),这样才能验证我们的代码是否正确,是否符合逻辑结果的预期。

1,非静态方法Mock
public class UserService {

    @Autowired
    private UserDao userDao;

    public UserInfo getUserInfoById(long id) {
        List<UserInfo> userInfoList = userDao.getAllUserInfo();
        for (UserInfo userInfo : userInfoList) {
            if (userInfo.getId()==id){
                return userInfo;
            }
        }
        return null;
    }
}

public class UserDao {

    public List<UserInfo> getAllUserInfo(){
        return null;
    }
}

Junit单测:

@ExtendWith({MockitoExtension.class})
public class UserInfoJunitTest {

    @Mock
    private UserDao userDao;

    @InjectMocks
    private UserService userService;

    @Before
    public void before(){
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void userinfoTest(){
        // 准备参数
        List<UserInfo> userInfoList = new ArrayList<>();
        UserInfo userInfo1 = new UserInfo();
        userInfo1.setId(0);
        userInfo1.setName("小明");

        UserInfo userInfo2 = new UserInfo();
        userInfo1.setId(1);
        userInfo1.setName("小强");

        userInfoList.add(userInfo1);
        userInfoList.add(userInfo2);

        // mock数据
        Mockito.when(userDao.getAllUserInfo()).thenReturn(userInfoList);
        UserInfo userInfo = userService.getUserInfoById(1);

        // 验证结果
        Assertions.assertEquals(userInfoList.get(1),userInfo);
    }
}

Spock单测

class UserInfoSpec extends Specification {

    def userService = new UserService();

    def "getUserInfoById"() {
        given: "准备参数"
        def user1 = new UserInfo(id: 0, name: "小明")
        def user2 = new UserInfo(id: 1, name: "小强")

        and: "mock数据"
        def userDao = Mock(UserDao)
        Whitebox.setInternalState(userService, "userDao", userDao)
        userDao.getAllUserInfo() >> [user1, user2]

        when: "方法调用"
        def response = userService.getUserInfoById(1)

        then: "结果验证"
        with(response) {
            id == 1
            name == "小强"
        }
    }
}

**given … when … then …**语句块:given为条件,when为执行方法,then为结果验证。

when … then 通常是成对出现的,;‘它代表着当执行了 when 块中的操作,会出现 then 块中的期望。

比较明显,上边的JUnit单元测试代码冗余,缺少结构层次,可读性差,随着后续的迭代,势必会导致代码的堆积,维护成本会变得越来越高。下面的单元测试代码Spock会强制要求使用givenwhenthen这样的语义标签(至少一个),否则编译不通过,这样就能保证代码更加规范,结构模块化,边界范围清晰,可读性强,便于扩展和维护。而且使用了自然语言描述测试步骤,让非技术人员也能看懂测试代码(given表示输入条件,when触发动作,then验证输出结果)。

Spock自带的Mock语法也非常简单:

def userDao = Mock(UserDao)使用Spock自导的Mock方法构造一个UserDao的Mock对象。

Whitebox.setInternalState(userService, "userDao", userDao)给userService对象中的userDao属性赋值刚才mock出来的userDao。

userDao.getAllUserInfo() >> [user1, user2]

两个右箭头>>表示模拟userDao.getAllUserInfo()接口的返回结果,再加上使用的Groovy语言,可以直接使用[]中括号表示返回的是List类型。

如果要指定返回多个值的话,可以使用3个右箭头>>>,比如:studentDao.getAllUserInfo() >>> [[user1,user2],[user3,user4],[user5,user6]]

每次调用userDao.getAllUserInfo()方法返回不同的值。

2,静态方法Mock

在Spock中可以通过powermock去模拟静态方法、final方法、私有方法等。

但是junit5不支持powermock,只能使用junit4来写

Junit单测

@RunWith(PowerMockRunner.class)
@PrepareForTest(IDNumberUtils.class)
public class IDNumberUtilsStaticTest {

    @InjectMocks
    private UserService userService;

    @Before
    public void setup(){
        PowerMockito.mockStatic(IDNumberUtils.class);
    }

    @Test
    public void testStatic(){

        // 准备参数
        Map<String,String> map = new HashMap<>();
        map.put("birthday","1992-08-09");
        map.put("age","29");

        // mock
        PowerMockito.when(IDNumberUtils.getBirthAge(Mockito.anyString())).thenReturn(map);
        int age = userService.getUserAgeByCardId("123");

        // 验证结果
        Assertions.assertEquals(29,age);
    }
}

Spock单测:

powermock的PowerMockRunner继承自Junit,所以使用powermock的@PowerMockRunnerDelegate()注解可以指定Spock的父类Sputnik去代理运行power mock,这样就可以在Spock里使用powermock去模拟静态方法、final方法、私有方法等

@PrepareForTest(IDNumberUtils.class)
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
class IDNumberUtilsStaticSpec extends Specification {

    def userService = new UserService();

    void setup() {
        PowerMockito.mockStatic(IDNumberUtils.class)
    }

    def "getAge"() {

        given: "准备参数"
        def map = [birthday: "1992-08-09", age: "29"]

        and: "mock"
        PowerMockito.when(IDNumberUtils.getBirthAge(Mockito.anyString())).thenReturn(map)

        when: "方法调用"
        def respsnse = userService.getUserAgeByCardId("aa")

        then: "结果验证"
        with(respsnse){
            respsnse == 29
        }
    }
}

当使用powermock来mock静态方法的时候,必须加注解@PrepareForTest@RunWith。注解@PrepareForTest里写的类是静态方法所在的类。

when模块里是真正调用要测试方法的入口userService.getUserAgeByCardId()

then模块作用是验证被测方法的结果是否正确,符合预期值,所以这个模块里的语句必须是boolean表达式,类似于JUnit的assert断言机制,但不必显示地写assert,这也是一种约定优于配置的思想。

then块中使用了Spock的with功能,可以验证返回结果response对象内部的多个属性是否符合预期值,这个相对于JUnit的assertNotNullassertEquals的方式更简单一些。

五,if…else多分支场景

在我们写业务代码时,经常会写到比较复杂的业务,对应的代码实现会走不通的逻辑,这样的 if else 嵌套代码因为业务的原因很难避免,如果要测试这样的代码,保证覆盖到每一个分支逻辑的话,使用传统的Junit单元测试代码写起来会很痛苦和繁琐。

业务代码:

public class TypeCodeUtil {

    public static String getTypeNameByCode(int code){
        String result = "";
        switch (code){
            case 1:
                result = "name1";
                break;
            case 2:
                result = "name2";
                break;
            case 3:
                result = "name3";
                break;
            case 4:
                result = "name4";
                break;
            case 5:
                result = "name5";
                break;
            default:
                break;
        }
        return result;
    }
}

junit单测:

public class TypeCodeUtilTest {

    @ParameterizedTest
    @MethodSource("getTypeCodeParams")
    public void testTypeCode(int code, Predicate<String> predicate){
        String typeNameByCode = TypeCodeUtil.getTypeNameByCode(code);
        Assertions.assertTrue(predicate.test(typeNameByCode));
    }

    public static Object[] getTypeCodeParams(){
        return new Object[]{
                new Object[]{
                        1,(Predicate<String>) value -> "name1".equals(value)
                },
                new Object[]{
                        2,(Predicate<String>) value -> "name2".equals(value)
                },
                new Object[]{
                        3,(Predicate<String>) value -> "name3".equals(value)
                },
                new Object[]{
                        4,(Predicate<String>) value -> "name4".equals(value)
                },
                new Object[]{
                        5,(Predicate<String>) value -> "name5".equals(value)
                },
                new Object[]{
                        6,(Predicate<String>) value -> "".equals(value)
                },

        };
    }
}

spock单测:

class TypeCodeUtilSpec extends Specification {

    @Unroll
    def "code:#code,name:#name"() {
        expect:
        TypeCodeUtil.getTypeNameByCode(code) == name
        where:
        code || name
        1    || "name1"
        2    || "name2"
        3    || "name3"
        4    || "name4"
        5    || "name5"
        6    || ""
    }
}

吃透单元测试:Spock单元测试框架的应用与实践

where模块第一行代码是表格的列名,多个列使用|单竖线隔开,||双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:

输入参数1 | 输入参数2 || 输出结果1 | 输出结果2

表格的每一行代表一个测试用例,即被测方法执行了6次,每次的输入和输出都不一样,刚好可以覆盖全部分支情况。

解析点:

  1. @Unroll注解:表示展开where标签下面的每一行测试用例,都作为一个单独的测试用例来运行。
  2. 方法名:#code和#name,把请求参数值和返回结果值的字符串动态替换掉,#号后面的变量是在方法内部定义的,实现占位符的功能。
  3. expect标签:when + then标签的组合,即 “什么时候做什么 + 验证什么结果” 组合起来。

六,异常测试

业务代码,这个大家应该都很熟悉,针对这种抛出多个不同错误码和错误信息的异常。如果使用JUnit的方式测试,会比较麻烦。如果是单个异常还好,如果是多个的话,测试代码就不太好写。

@Data
@AllArgsConstructor
public class APIException extends RuntimeException{
    private String errorCode;
    private String errorMessage;
}

@Service
public class ValidateService {

    public void validateUser(UserInfo userInfo){
        if (userInfo == null){
            throw new APIException("1001","userInfo is null");
        }else if (userInfo.getId()==0){
            throw new APIException("1002","id is not legal");
        }else if (userInfo.getName() == null || "".equals(userInfo.getName())){
            throw new APIException("1003","name is not legal");
        }
    }
}

Junit单测:在junit3中可以通过一次次的方法调用并捕获异常来进行测试。这种方式的缺陷是显而易见的。如果有100个异常测试,那么这些调用要写100遍简直要疯掉。

而在junit4中,可以使用ExpectedException@Test这两种方式,但是都有缺陷,@Test方式不能指定断言的异常属性,比如codemessageExpectedException的方式也只提供了expectMessage的API,对自定义的code不支持,尤其像上面的有很多分支抛出多种不同异常码的情况。

在junit4中,可以使用Assertions.assertThrows方法来验证异常,但是依然有上面方式的缺陷。

public class ExceptionTest {

    private ValidateService validateService = new ValidateService();

    /**
     * junit3
     */
    @Test
    public void testException1(){

        try {
            validateService.validateUser(null);
        }catch (APIException e){
            Assert.assertEquals(e.getErrorCode(),"1001");
            Assert.assertEquals(e.getErrorMessage(),"userInfo is null");
        }

        try {
            validateService.validateUser(new UserInfo(0,""));
        }catch (APIException e){
            Assert.assertEquals(e.getErrorCode(),"1002");
            Assert.assertEquals(e.getErrorMessage(),"id is not legal");
        }

        try {
            validateService.validateUser(new UserInfo(1,""));
        }catch (APIException e){
            Assert.assertEquals(e.getErrorCode(),"1003");
            Assert.assertEquals(e.getErrorMessage(),"name is not legal");
        }

    }

    /**
     * junit 4
     */
    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @Test
    public void testException2(){
        expectedException.expect(APIException.class);
        expectedException.expectMessage("userInfo is null");
        validateService.validateUser(null);
    }

    @Test(expected = APIException.class)
    public void testException3(){
        validateService.validateUser(null);
    }

    /**
     * junit5
     */
    @Test
    public void testException4(){
        Throwable throwable = Assertions.assertThrows(APIException.class,()->{
            validateService.validateUser(null);
        });
        Assertions.assertEquals("userInfo is null",throwable.getMessage());
    }
}

Spock单测:

class ExceptionSpec extends Specification {

    def validateService = new ValidateService()

    @Unroll
    def "验证UserInfo"() {

        when: "调用校验方法"
        validateService.validateUser(user)

        then: "捕获异常并设置需要验证的异常值"
        def exception = thrown(expectedException)
        exception.errorCode == expectedErrCode
        exception.errorMessage == expectedMessage

        where: "验证用户的合法性"
        user                || expectedException | expectedErrCode | expectedMessage
        null                || APIException      | "1001"          | "userInfo is null"
        new UserInfo(0, "") || APIException      | "1002"          | "id is not legal"
        new UserInfo(1, "") || APIException      | "1003"          | "name is not legal"
    }
}

Spock内置thrown()方法,可以捕获调用业务代码抛出的预期异常并验证,再结合where表格的功能,可以很方便的覆盖多种自定义业务异常。

then标签里用到了Spock的thrown()方法,这个方法可以捕获我们要测试的业务代码里抛出的异常。thrown()方法的入参expectedException,是我们自己定义的异常变量,这个变量放在where标签里就可以实现验证多种异常情况的功能。expectedException类型调用validateUser方法里定义的APIException异常,可以验证它所有的属性,errorCodeerrorMessage是否符合预期值。

七,void方法测试

根据UserInfo中的IdNo属性,调用年龄计算工具,计算出年龄后赋值给UserInfo的age属性。

public class UserService {

    @Autowired
    private IDNumberUtil idNumberUtil;
		/**
     * 根据idNo计算年龄然后赋值给age属性
     * @param list
     */
    public void setAgeTest(List<UserInfo> list) {
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        for (UserInfo userInfo : list) {
            int age = idNumberUtil.getAge(userInfo.getCardId());
            userInfo.setAge(age);
        }
    }
}

  

Spock单测:

class VoidMethodSpec extends Specification {

    def userService = new UserService()

    def idNumberUtil = Mock(IDNumberUtil)

    def "测试void方法"() {

        given: "准备参数"
        def user1 = new UserInfo(id: 1, cardId: "410225199208091234")
        def user2 = new UserInfo(id: 2, cardId: "410225199208091234")
        def list = [user1, user2]

        and: "属性设置"
        Whitebox.setInternalState(userService, "idNumberUtil", idNumberUtil)

        when: "调用方法"
        userService.setAgeTest(list)

        then: "验证调用获取年龄是否符合预期: 一共调用2次, 第一次输出29, 第二次是28"
        2 * idNumberUtil.getAge(_) >> 29 >> 28

        and: "验证年龄计算后的结果是否符合预期"
        with(list) {
            list[0].age == 29
            list[1].age == 28
        }
    }
}

八,dao层测试

DAO层的测试有些不太一样,不能再使用Mock,否则无法验证SQL是否正确。对于DAO测试有一般最简的方式是直接使用@SpringBootTest注解启动测试环境,通过Spring创建Mybatis、Mapper实例,但这种方式并不属于单元测试,而是集成测试范畴了,因为当启用@SpringBootTest时,会把整个应用的上下文加载进来。不仅耗时时间长,而且一旦依赖环境上有任何问题,可能会影响启动,进而影响DAO层的测试。最后,需要到数据库尽可能隔离,因为如果大家都使用同一个Test环境的数据的话,一旦测试用例编写有问题,就可能会污染Test环境的数据。

针对以上场景,可采用以下方案:文章来源地址https://www.toymoban.com/news/detail-484973.html

  1. 通过MyBatis的SqlSession启动mapper实例(避免通过Spring启动加载上下文信息)。
  2. 通过内存数据库(如H2)隔离大家的数据库连接(完全隔离不会存在互相干扰的现象)。
  3. 通过DBUnit工具,用作对于数据库层的操作访问工具。
  4. 通过扩展Spock的注解,提供对于数据库创建和数据Data加载的方式。
1,通过MapperUtil工具类启动MyBatis的SqlSession获取Mapper实例
public class MapperUtil {

    private static final String MAPPER_CONFIG_LOCATIONS = "cn/huolala/link/svc/support/mapper-config.xml";

    /**
     * 通过MyBatis的SqlSession启动mapper实例
     * @param type
     * @param dataSource
     * @param <T>
     * @return
     * @throws IOException
     */
    public static <T> T getMapper(Class<T> type, DataSource dataSource) throws IOException {
        
        // 获取资源
        InputStream inputStream = Resources.getResourceAsStream(MAPPER_CONFIG_LOCATIONS);
        JdbcTransactionFactory jdbcTransactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("development", jdbcTransactionFactory, dataSource);
        
        // 启动SqlSessionFactory获取mapper
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSessionFactory.getConfiguration().setEnvironment(environment);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        return sqlSession.getMapper(type);
    }
}
2,通过内存数据库(H2)隔离大家的数据库连接
class SpecUtils {
  
		/**
     * 初始化H2数据库
     * @return
     */
    static DataSource inMemoryDataSource() {
        return new HikariDataSource().with { dataSource ->
            dataSource.driverClassName = 'org.h2.Driver'
            dataSource.jdbcUrl = 'jdbc:h2:file:/Users/zhaosongbo/spock'
            dataSource.username = 'root'
            dataSource.password = 'test'
        }
    }

 		/**
     * 创建相应的表结构
     * @param dataSource
     */
    static void createAppMainTable(javax.sql.DataSource dataSource){
        assert dataSource
        new Sql(dataSource).execute("DROP TABLE IF EXISTS app_main; CREATE TABLE `app_main` (\n" +
                "  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自动增长主键',\n" +
                "  `app_id` varchar(32) NOT NULL COMMENT '分配给接入方的唯一标识',\n" +
                "  `app_key` varchar(32) DEFAULT NULL COMMENT 'appkey',\n" +
                "  `app_secret` varchar(64) NOT NULL COMMENT '分配给接入方的密钥'\n" +
                ") ;")
    }

}
3,Spock来编写相应的测试代码
class AppMainDB2Spec extends Specification {

    def dataSource

    /**
     * 相当于junit的@efore
     * @return
     */
    def setup() {
        dataSource = inMemoryDataSource3()
        createAppMainTable(dataSource)
    }

    /**
     * 构造数据
     * @return
     */
    @DbUnit(content = {
        app_main(id: 1001,app_id: "appId1",app_key: "appKey1",app_secret: "appSecret1")
        app_main(id: 1002,app_id: "appId2",app_key: "appKey2",app_secret: "appSecret2")
        app_main(id: 1003,app_id: "appId3",app_key: "appKey3",app_secret: "appSecret3")
    }, configure = { IDatabaseTester it ->
        it.setUpOperation = DatabaseOperation.CLEAN_INSERT
        it.tearDownOperation = DatabaseOperation.TRUNCATE_TABLE
    }, datasourceProvider = { dataSource })

    def "dao层测试"() {

        given: "获取mapper"
        def appMainMapper = MapperUtil.getMapper(AppMainMapper.class,dataSource)

        when: "执行"
        def result = appMainMapper.appSecretByAppId("appId1")

        then: "验证数据"
        result == 'appSecret1'
    }
}

到了这里,关于吃透单元测试:Spock单元测试框架的应用与实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 前端单元测试与自动化测试实践

    在前端开发中,单元测试和自动化测试是保证代码质量和稳定性的重要手段。通过编写和执行测试用例,可以及早发现代码中的问题,并确保代码在不同环境下的正确运行。本文将介绍前端单元测试和自动化测试的实践,并通过一个示例说明其重要性和具体操作。 前端单元测

    2024年02月12日
    浏览(49)
  • 单元测试的实践与思考

    之前一直有一个想法:将测试过程的每个重要环节都进行拆解,然后详细说明这个环节重点要做的事情,为什么要做这些事,以及注意事项。在星球群里和几位同学聊到了这个事情,有同学提议可否将单元测试环节加进来,斟酌一番,觉得还是很有必要的,就有了今天的这篇

    2024年02月05日
    浏览(40)
  • 单元测试的最佳实践

    本文 1-2章 是单测的介绍,如果需要直接看整合教程,直接跳至 第3章 看干货; 在复杂系统的开发中,我们经常需要做单元测试。但又由于单元测试没有形成标准,而经常遇到这样的问题: 直接写一个main函数对需要的方法进行测试 单元测试过于复杂,有时候又懒得做单元测

    2024年02月04日
    浏览(38)
  • 单元测试规范和实践

    软件测试按照阶段划分,一般可以分为如下四类: 单元测试 单元测试是对软件组成单元进行测试。其目的是检验软件基本组成单位的正确性。测试的对象是软件设计的最小单位:函数。常常由开发人员依据详细的设计文档进行测试,包含接口测试、局部数据结构测试、路径

    2024年02月01日
    浏览(51)
  • Pytest自动化测试框架---(单元测试框架)

    unittest是python自带的单元测试框架,它封装好了一些校验返回的结果方法和一些用例执行前的初始化操作,使得单元测试易于开展,因为它的易用性,很多同学也拿它来做功能测试和接口测试,只需简单开发一些功能(报告,初始化webdriver,或者http请求方法)便可实现。 但自

    2024年02月14日
    浏览(70)
  • 单元测试之 - Spring框架提供的单元/集成测试注解

    Spring框架提供了很多注解来辅助完成单元测试和集成测试(备注:这里的集成测试指容器内部的集成测试,非系统间的集成测试),先看看Spring框架提供了哪些注解以及对应的作用。 @RunWith(SpringRunner.class) / @ExtendWith(SpringExtension.class) : 用于在测试类中启用 Spring 框架的支持。

    2024年02月14日
    浏览(46)
  • Day14:单元测试、Junit单元测试框架、反射、注解

    针对最小的功能单元(方法)进行正确性测试 编写正规的单元测试框架 传统的无法执行自动化测试,且无法得到测试报告 Junit的作用: 测试类取名:原类名+Test(大驼峰) 测试方法取名:test+原函数名称(小驼峰) 测试方法:必须public,无参,无返回值 测试方法上面必须加

    2024年04月14日
    浏览(63)
  • GoogleTest : 测试框架(单元测试)

    googletest: GoogleTest - Google Testing and Mocking Framework googletest 是一个由 Google 的测试技术团队开发的测试框架,它考虑到了谷歌的特定需求和限制。无论你使用的是 Linux、Windows 还是 Mac,只要你编写 C++ 代码,googletest 都可以帮到你。它 支持任何类型的测试 ,不只是单元测试。 独立

    2024年02月06日
    浏览(41)
  • ChatGPT生成单元测试实践(Golang)

    目前gpt本质上是续写,所以在待测函数定义清晰的情况下,单元测试可以适当依赖它进行生成。 收益是什么: 辅助生成测试用例测试代码,降低单元测试编写的心智成本 辅助code review,帮助发现代码显式/潜在问题 本文测试环境: gpt: gpt-3.5-turbo go:go 1.17 本文实践场景:企业

    2023年04月20日
    浏览(47)
  • Jest单元测试Vue项目实践

    ​ 做单元测试的优点: 1.减少bug避免低级错误 2.提高代码运行质量 3.快速定位问题 4.减少调试时间,提高开发效率 5.便于重构 Jest安装: 配置 vueCli内置了一套jest配置预置文件,一般情况下直接引用即可,如有特殊配置可见下文配置释意。 配置项目释意 module.exports = { 相关

    2024年02月12日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包