Should we test the DAO layer?

Should the DAO layer be tested? #

There is a lot of discussion online about whether unit testing should include testing at the DAO layer. The author feels that for some businesses that are mainly CRUD, the service layer and controller layer will be very thin, and the main logic falls on the mapper. At this time, it does not make much sense to write single tests on the service layer and controller layer. You can only write a single test of the mapper layer.

On the other hand, the testing of the mapper layer can effectively avoid some low-level SQL errors.

Define Single Test#

A unit test is a test that targets only one unit, for example, each public function of a Service class. All places where this function calls external dependencies need to be isolated, such as dependencies on external classes, or requests to a certain server.
In other words, unit testing only tests the logic of a function of the current class itself, and does not involve external logic. So executing a single test should be very fast.

Commonly used dependencies for single testing in Java are mainly divided into test frameworks and Mock frameworks. The test framework is a framework for executing and managing test methods, generally using JUnit. The Mock framework is used to simulate external dependencies and isolate all external dependencies of the function being tested.

Some Misunderstandings#

I have seen too many single-test tutorials on the Internet, and they are all written in a mess. Publishing an article without even understanding the concept of single test is really misleading.
Regarding common misunderstandings, this blog lists it well: How to write good unit tests: Mock out of the database + do not use @SpringBootTest

The most critical point is not to use @SpringBootTest(classes=XXXApplication.class) to annotate test classes. This will directly start a springboot process. For slightly more complex projects, it will take at least 1 minute to run. If the project uses middleware such as remote configuration center and SOA, it is recommended to go out and make a cup of tea.
So why doesn’t everyone want to write a single test? After waiting for so long, the tea has become cold when people leave. But in fact these are wrong implementation techniques. The following article explains examples of test classes at different integration levels in SpringBoot projects: Testing in Spring Boot | Baeldung
In general, clearly distinguish the difference between integration testing and unit testing. Don’t write unit tests as integration tests.

DAO layer test implementation#

Selection#

The following article summarizes it well: Writing valuable unit tests – Alibaba Cloud Developer Community
Database testing needs to ensure that the test will not affect the external environment, and the generated data needs to be automatically destroyed after the test is completed. Generally there are several methods:

  1. Connect to the development environment database and roll back after testing. Not recommended
  2. Use docker container: testContainer. Start the mysql container during testing and automatically recycle it after completion. Disadvantages: Each test machine needs to install docker and download the container. This results in:
    1. Need to push other developers to install the image
    2. Need to promote devops online CI/CD pipeline to install docker. (give up)
  3. Using an in-memory database, data is not persisted. The more commonly used ones are h2.

If it is a personal development project, the integrated deployment pipeline may not be used. You can try to use testContainer, because it can not only connect to mysql test, but also simulate some middleware such as redis, mq, etc. However, for complex projects developed by large teams, it is recommended to use an in-memory database directly.
In addition, Mybatis provides a test dependency package that integrates h2, refer to: mybatis-spring-boot-test-autoconfigure – Introduction. However, the disadvantage is that it needs to rely on different versions of springboot. The springboot version used in the project developed by the author is older and should not be updated, so I directly configure h2 manually.

code#

Note: The following code was implemented as a reference from an article somewhere, but I don’t remember the specific source.

We need to manually create 4 beans to inject:

  1. DataSource, used for jdbc connection to the corresponding h2 database.
  2. Server. h2’s gui server service can be used to connect to the database to view data. not necessary.
  3. SqlSessionFactory. Create a sqlSessionFactory for mybatis and indicate the location of the mapper’s xml file
  4. MapperScannerConfigurer. Used to generate proxy beans from the mapper interface in mybatis.
    Some points to note:
  5. @ComponentScan needs to fill in the location of the mapper interface in the current project
  6. When creating a DataSource, addScript() specifies the sql for creating tables and initializing data prepared by yourself. The path is in test/resources/db/schema-h2.sql
  7. When creating sqlSessionFactory, specify the mapper.xml file in resources.
  8. When creating mapperScannerConfigurer, specify the package of the mapper interface and the name of the bean of the factory created in the previous step. The default name is used here, which is the name of the method.
@Configuration
@ComponentScan({ "com.my.app.mapper" })
public class BaseTestConfig {
    @Bean()
    public DataSource dataSource() {
        EmbeddedDatabaseBuilder databaseBuilder = new EmbeddedDatabaseBuilder();

        return databaseBuilder
                .setType(EmbeddedDatabaseType.H2)
                //Initialize the table creation statement at startup
                .addScript("classpath:db/schema-h2.sql")
                .build();
    }

    @Bean(name = "h2WebServer", initMethod = "start", destroyMethod = "stop")
    //Start an H2 web server. You can access the H2 content through localhost:8082 during debugging.
    //JDBC URL: jdbc:h2:mem:testdb
    //User Name: sa
    //Password: None
    //Note that if you use breakpoints, the breakpoint type (Suspend Type) must be set to Thread and not All, otherwise the web server cannot access it normally!
    public Server server() throws Exception {
        //Start a web server on port 8082
        return Server.createWebServer("-web", "-webAllowOthers", "-webDaemon", "-webPort", "8082");
    }

    @Bean()
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        //Load all sqlmapper files
        Resource[] mapperLocations = resolver.getResources("classpath*:mapper/*.xml");
        sessionFactory.setMapperLocations(mapperLocations);
        return sessionFactory.getObject();
    }

    @Bean()
    public MapperScannerConfigurer mapperScannerConfigurer() {
        //You only need to write the DAO interface, no implementation class is needed, and the agent is dynamically generated at runtime
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        configurer.setBasePackage("com.my.app.mapper");
        configurer.setSqlSessionFactoryBeanName("sqlSessionFactory");
        return configurer;
    }

}

After creating such a Configuration class, the subsequent MapperTest class only needs to use @Import to introduce this configuration class, or put all the annotations on a base class so that the subsequent mapper test classes can inherit it. With this base class, there is no need to add annotations to each test class:

@RunWith(SpringJUnit4ClassRunner.class)
@Import(BaseTestConfig.class)
public class BaseMapperTest {
    @Autowired
    private MyMapper myMapper;
    @Test
    public void test(){
        Object o = myMapper.selectOne();
        assertNotNull(o);
    }
}