• 单元测试


    单元测试 - 探索java web 单元测试的正确姿势

     一丶起因

      笔者一直听闻TDD,自动化测试等高大上的技术名词, 向往其中的便利之处, 但一直求而不得, 只因项目中有各种依赖的存在,其中最大的依赖便是数据库. java web 项目大部分都是写sql语句, 不依赖数据库, 便测试不了sql语句的正确性, 但依赖数据库又有种种不变之处. 除此之外, 还有种种类与类之间的依赖关系,很不方便. 遗憾的是, 网上各种文章参差不齐, 笔者所参与的项目很少甚至没有单元测试, 修改代码, 如履薄冰. 在苦思不得其解之际, 向优秀开源项目mybatis求取经验, 终获得一些答案.

     

    二丶实践思路

       mybatis使用单元测试的方式是使用内存数据库做单元测试,单元测试前,先根据配置以及数据库脚本,初始化内存数据库,然后再使用内存数据库测试.所以,笔者也是采用这种思路.

      除此之外, 还有各种类与类之间依赖关系, 笔者依据mybatis以及spring选择使用mockito框架mock解决

      所以选用的工具有 hsql内存数据库, mockito mock工具, junit单元测试工具, spring-boot-test子项目

    三丶实施测试

      1. 在pom.xml添加hsql 以及mockito

           <dependency>
                <groupId>org.hsqldb</groupId>
                <artifactId>hsqldb</artifactId>
                <version>2.5.0</version>
                <scope>test</scope>
            </dependency>
    
           <dependency>
                <groupId>org.mockito</groupId>
                <artifactId>mockito-core</artifactId>
                <version>3.1.0</version>
                <scope>test</scope>
            </dependency>

      2. 在test/resources/databases/jpetstore 下添加hsql 数据源的配置, 其中显示设置sql.syntax_mys=true 是对mysql的支持

    driver=org.hsqldb.jdbcDriver
    ## 配置hsql最大程度兼容mysql
    url=jdbc:hsqldb:.;sql.syntax_mys=true
    username=sa
    password=

      以及在该文件夹下配置初始化化mysql测试数据的脚本

      

      3. 配置初始化内存测试库

        BaseDataTest.java  来源于mybatis,用于运行sql脚本,初始化测试库

    public abstract class BaseDataTest {
    
      public static final String BLOG_PROPERTIES = "org/apache/ibatis/databases/blog/blog-derby.properties";
      public static final String BLOG_DDL = "org/apache/ibatis/databases/blog/blog-derby-schema.sql";
      public static final String BLOG_DATA = "org/apache/ibatis/databases/blog/blog-derby-dataload.sql";
    
      public static final String JPETSTORE_PROPERTIES = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb.properties";
      public static final String JPETSTORE_DDL = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb-schema.sql";
      public static final String JPETSTORE_DATA = "org/apache/ibatis/databases/jpetstore/jpetstore-hsqldb-dataload.sql";
    
      public static UnpooledDataSource createUnpooledDataSource(String resource) throws IOException {
        Properties props = Resources.getResourceAsProperties(resource);
        UnpooledDataSource ds = new UnpooledDataSource();
        ds.setDriver(props.getProperty("driver"));
        ds.setUrl(props.getProperty("url"));
        ds.setUsername(props.getProperty("username"));
        ds.setPassword(props.getProperty("password"));
        return ds;
      }
    
      public static PooledDataSource createPooledDataSource(String resource) throws IOException {
        Properties props = Resources.getResourceAsProperties(resource);
        PooledDataSource ds = new PooledDataSource();
        ds.setDriver(props.getProperty("driver"));
        ds.setUrl(props.getProperty("url"));
        ds.setUsername(props.getProperty("username"));
        ds.setPassword(props.getProperty("password"));
        return ds;
      }
    
      public static void runScript(DataSource ds, String resource) throws IOException, SQLException {
        try (Connection connection = ds.getConnection()) {
          ScriptRunner runner = new ScriptRunner(connection);
          runner.setAutoCommit(true);
          runner.setStopOnError(false);
          runner.setLogWriter(null);
          runner.setErrorLogWriter(null);
          runScript(runner, resource);
        }
      }
    
      public static void runScript(ScriptRunner runner, String resource) throws IOException, SQLException {
        try (Reader reader = Resources.getResourceAsReader(resource)) {
          runner.runScript(reader);
        }
      }
    
      public static DataSource createBlogDataSource() throws IOException, SQLException {
        DataSource ds = createUnpooledDataSource(BLOG_PROPERTIES);
        runScript(ds, BLOG_DDL);
        runScript(ds, BLOG_DATA);
        return ds;
      }
    
      public static DataSource createJPetstoreDataSource() throws IOException, SQLException {
        DataSource ds = createUnpooledDataSource(JPETSTORE_PROPERTIES);
        runScript(ds, JPETSTORE_DDL);
        runScript(ds, JPETSTORE_DATA);
        return ds;
      }
    }

        配置数据源, 初始化数据库, 以及配置生成mapper

    /**
     * 准备内存数据库中的数据
     * @author TimFruit
     * @date 19-11-17 上午10:50
     */
    @Configuration
    public class BaseDataConfig {
        public static final Logger logger=LoggerFactory.getLogger(BaseDataConfig.class);
    
        String dataPrefix= "databases/jpetstore/";
    
        //数据源
        @Bean("myDataSource")
        public DataSource createDataSource() {
            //创建数据源
            logger.info("创建数据源...");
            InputStream inputStream=FileUtil.getInputStream(dataPrefix+"jpetstore-hsql2mysql.properties");
            Properties properties=new Properties();
            try {
                properties.load(inputStream);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    
            HikariDataSource dataSource=new HikariDataSource();
            dataSource.setDriverClassName(properties.getProperty("driver"));
            dataSource.setJdbcUrl(properties.getProperty("url"));
            dataSource.setUsername(properties.getProperty("username"));
            dataSource.setPassword(properties.getProperty("password"));
    
    
            //准备数据
            logger.info("准备数据...");
            try {
                BaseDataTest.runScript(dataSource, dataPrefix+"jpetstore-mysql-schema.sql");
                BaseDataTest.runScript(dataSource, dataPrefix+"jpetstore-mysql-dataload.sql");
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            logger.info("准备数据完成...");
    
            return dataSource;
        }
    
    
    
        // mapper
        @Bean
        public SqlSessionFactory createSqlSessionFactoryBean (
                @Qualifier("myDataSource") DataSource dataSource,
                @Autowired MybatisProperties mybatisProperties) throws Exception {
    
            SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
            factory.setDataSource(dataSource);
            if (!ObjectUtils.isEmpty(mybatisProperties.resolveMapperLocations())) {
                factory.setMapperLocations(mybatisProperties.resolveMapperLocations());
            }
            return factory.getObject();
        }
    
    
    }

       4. spring-boot-test对mockito的支持

        使用@SpyBean和@MockBean修饰属性, spring会对该类型的bean进行spy或者mock操作, 之后装配该类型属性的时候, 都会使用spy或者mock之后的bean进行装配.  关于Mockito的使用可以看我前一篇文章

    //    @MockBean
        @SpyBean
        AccountMapper accountMapper;
    
        @SpyBean
        AccountService accountService;
    
    
        @Test
        public void shouldSelectAccount(){
    
            Account  mockAccount=createTimFruitAccount();
    
            //打桩
            doReturn(mockAccount)
                    .when(accountMapper)
                    .selectAccount(timfruitUserId);
    
            //测试service方法
            Account result=accountService.selectAccount(timfruitUserId);
    
            //验证
            Assert.assertEquals(mockAccount, result);
    
        }

        需要注意的时, 使用@SpyBean修饰Mapper类的时候, 需要设置mockito对final类型的支持, 否则会报"Mockito cannot mock/spy because : - final class"的异常

        设置方式如下:

        先在resources文件夹下,新建mockito-extensions文件夹,在该文件夹下新建名为org.mockito.plugins.MockMaker的文本文件,添加以下内容: 

    mock-maker-inline

       

      5. 对Mapper sql 进行单元测试

      避免各单元测试方法的相互影响, 主要利用数据库的事务, 测试完之后, 回滚事务, 不应影响其他测试

    @RunWith(SpringRunner.class)
    @SpringBootTest
    //加事务, 在单元测试中默认回滚测试数据, 各个测试方法互不影响
    // https://blog.csdn.net/qq_36781505/article/details/85339640
    @Transactional
    public class AccountMapperTests {
        @SpyBean
        AccountMapper accountMapper;
    
    
        @Test
        public void shouldSelectAccount(){
    
            //given 测试数据库中的数据
    
            //when
            Account result=accountMapper.selectAccount("ttx");
    
            //then
            Assert.assertEquals("ttx@yourdomain.com", result.getEmail());
    
        }
    
        @Test
        public void shouldInsertAccount(){
            //given
            Account timAccount=createTimFruitAccount();
            //when
            accountMapper.insertAccount(timAccount);
    
    
            //then
            Account result=accountMapper.selectAccount(timfruitUserId);
            Assert.assertEquals(timfruitUserId, result.getUserid());
            Assert.assertEquals(timAccount.getCity(), result.getCity());
            Assert.assertEquals(timAccount.getAddr1(), result.getAddr1());
            Assert.assertEquals(timAccount.getAddr2(), result.getAddr2());
        }
    
    
        @Test
        public void shouldUpdateCountry(){
            //given
            String userId="ttx";
            String country="万兽之国";
    
            //when
            accountMapper.updateCountryByUserId(userId, country);
    
            //then
            Account result=accountMapper.selectAccount(userId);
            Assert.assertEquals(country, result.getCountry());
    
        }
    
        @Test
        public void shouldDeleteAccount(){
            //given
            String userId="ttx";
            Account account=accountMapper.selectAccount(userId);
            Assert.assertTrue(account!=null);
    
    
    
            //when
            accountMapper.deleteAccountry(userId);
    
    
            //then
            account=accountMapper.selectAccount(userId);
            Assert.assertTrue(account==null);
        }
    
        @Test
        public void shouldSaveAccountBatch(){
    
            //given
            //update
            String userId1="ttx";
            String country1="天堂";
            Account account1=accountMapper.selectAccount(userId1);
            account1.setCountry(country1);
            //insert
            String userId2=timfruitUserId;
            String country2="中国";
            Account account2=createTimFruitAccount();
            account2.setCountry(country2);
    
            List<Account> accountList=Arrays.asList(account1,account2);
    
            //when
            accountMapper.saveAccountBatch(accountList);
    
            //then
            account1=accountMapper.selectAccount(userId1);
            Assert.assertEquals(country1, account1.getCountry());
    
    
            account2=accountMapper.selectAccount(userId2);
            Assert.assertEquals(country2, account2.getCountry());
    
    
        }
        
    }

       完整源码

    四丶后记

      1. 这里仅仅只是对java web 单元测试实践提供一种思路, 笔者尚未在真实项目中应用, 有可能存在一些坑, 如hsql对mysql的兼容性支持等.任重而道远

      2. 单元测试要想实施方便, 主要思路是除去各种依赖

      3. 多向优秀开源项目学习, 我所走之路, 前人早已开辟, 这其实也是看源码的一个好处


    补充(2020-04-25):

    使用内存数据库兼容性不好,流程不变,将内存数据库改为docker运行数据库更为可行

    人生没有彩排,每一天都是现场直播
  • 相关阅读:
    反射和内置方法重写
    封装
    接口与抽象类 、多态
    面向对象--继承和组合
    python对象
    模块导入
    python序列化模块
    time random sys os 模块
    python re模块和collections
    python各种推导式
  • 原文地址:https://www.cnblogs.com/timfruit/p/11878851.html
Copyright © 2020-2023  润新知