几年前,我是为我的数据访问代码编写单元测试的那些开发人员之一。 我正在孤立地测试所有内容,我对自己感到非常满意。 老实说,我认为自己做得很好。 哦,男孩,我错了! 这篇博客文章描述了为什么我们不应该为数据访问代码编写单元测试,并解释为什么我们应该用集成测试代替单元测试。 让我们开始吧。
单元测试错误问题的答案
我们为数据访问代码编写测试,因为我们想知道它可以按预期工作。 换句话说,我们想找到这些问题的答案:
- 是否将正确的数据存储到使用的数据库?
- 我们的数据库查询是否返回正确的数据?
单元测试可以帮助我们找到想要的答案吗? 好吧, 单元测试的最基本规则之一是单元测试不应使用诸如数据库之类的外部系统 。 此规则不适用于当前情况,因为存储正确信息和返回正确查询结果的责任由我们的数据访问代码和使用的数据库划分。 例如,当我们的应用程序执行单个数据库查询时,职责划分如下:
- 负责创建执行的数据库查询的数据访问代码。
- 数据库负责执行数据库查询,并将查询结果返回给数据访问代码。
问题是,如果我们将数据访问代码与数据库隔离,则可以测试数据访问代码是否创建了“正确的”查询,但是我们无法确保所创建的查询返回正确的查询结果。 这就是为什么单元测试不能帮助我们找到想要的答案 。
告诫故事:假装是问题的一部分
有段时间我为数据访问代码编写了单元测试。 当时我有两个规则:
- 每段代码都必须单独进行测试。
- 让我们使用模拟。
我当时在一个使用Spring Data JPA的项目中工作,而动态查询是通过使用JPA条件查询构建的。 如果您不熟悉Spring Data JPA,则可能需要阅读Spring Data JPA教程的第四部分,该教程介绍了如何使用Spring Data JPA创建JPA条件查询 。 无论如何,我创建了一个规范构建器类来构建Specification <Person>对象。 创建Specification <Person>对象后,将其转发给我的Spring Data JPA存储库,该存储库执行查询并返回查询结果。 规范构建器类的源代码如下所示:
import org.springframework.data.jpa.domain.Specification;import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;public class PersonSpecifications {public static Specification<Person> lastNameIsLike(final String searchTerm) {return new Specification<Person>() {@Overridepublic Predicate toPredicate(Root<Person> personRoot, CriteriaQuery<?> query, CriteriaBuilder cb) {String likePattern = getLikePattern(searchTerm); return cb.like(cb.lower(personRoot.<String>get(Person_.lastName)), likePattern);}private String getLikePattern(final String searchTerm) {return searchTerm.toLowerCase() + "%";}};}
}
让我们看一下“验证”规范构建器类创建“正确”查询的测试代码。 请记住,我是按照自己的规则编写此测试类的,这意味着结果应该很棒。 PersonSpecificationsTest类的源代码如下所示:
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.jpa.domain.Specification;import javax.persistence.criteria.*;import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.*;public class PersonSpecificationsTest {private static final String SEARCH_TERM = "Foo";private static final String SEARCH_TERM_LIKE_PATTERN = "foo%";private CriteriaBuilder criteriaBuilderMock;private CriteriaQuery criteriaQueryMock;private Root<Person> personRootMock;@Beforepublic void setUp() {criteriaBuilderMock = mock(CriteriaBuilder.class);criteriaQueryMock = mock(CriteriaQuery.class);personRootMock = mock(Root.class);}@Testpublic void lastNameIsLike() {Path lastNamePathMock = mock(Path.class); when(personRootMock.get(Person_.lastName)).thenReturn(lastNamePathMock);Expression lastNameToLowerExpressionMock = mock(Expression.class);when(criteriaBuilderMock.lower(lastNamePathMock)).thenReturn(lastNameToLowerExpressionMock);Predicate lastNameIsLikePredicateMock = mock(Predicate.class);when(criteriaBuilderMock.like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN)).thenReturn(lastNameIsLikePredicateMock);Specification<Person> actual = PersonSpecifications.lastNameIsLike(SEARCH_TERM);Predicate actualPredicate = actual.toPredicate(personRootMock, criteriaQueryMock, criteriaBuilderMock);verify(personRootMock, times(1)).get(Person_.lastName);verifyNoMoreInteractions(personRootMock);verify(criteriaBuilderMock, times(1)).lower(lastNamePathMock);verify(criteriaBuilderMock, times(1)).like(lastNameToLowerExpressionMock, SEARCH_TERM_LIKE_PATTERN);verifyNoMoreInteractions(criteriaBuilderMock);verifyZeroInteractions(criteriaQueryMock, lastNamePathMock, lastNameIsLikePredicateMock);assertEquals(lastNameIsLikePredicateMock, actualPredicate);}
}
这有意义吗? 没有! 我必须承认,此测试对任何人都没有价值,应该尽快删除。 该测试存在三个主要问题:
- 它不能帮助我们确保数据库查询返回正确的结果。
- 很难理解并使情况更糟,它描述了查询的构建方式,但没有描述查询应返回的内容。
- 这样的测试很难编写和维护。
事实是,此单元测试是不应编写的测试的教科书示例。 它对我们没有任何价值,但我们仍然必须维护它。 因此, 这是浪费! 但是,如果我们为数据访问代码编写单元测试,就会发生这种情况。 我们最终得到了一个测试套件,无法测试正确的东西。
数据访问测试正确完成
我是单元测试的忠实拥护者,但是在某些情况下,它并不是工作的最佳工具。 这是其中一种情况。 数据访问代码与使用的数据存储有非常密切的关系。 这种关系是如此紧密,以至于没有数据存储,数据访问代码本身就没有用。 这就是为什么将我们的数据访问代码与使用的数据存储区分开来是没有意义的。 解决这个问题很简单。 如果我们要为数据访问代码编写全面的测试,则必须将数据访问代码与使用的数据存储一起进行测试。 这意味着我们必须忘记单元测试并开始编写集成测试 。 我们必须了解,只有集成测试才能验证
- 我们的数据访问代码创建正确的数据库查询。
- 我们的数据库返回正确的查询结果。
如果您想知道如何编写针对Spring支持的存储库的集成测试,则应阅读我的博客文章“ Spring Data JPA教程:集成测试” 。 它描述了如何为Spring Data JPA存储库编写集成测试。 但是,在为使用关系数据库的任何存储库编写集成测试时,可以使用相同的技术。 例如, 为测试“ 将jOOQ与Spring结合使用”教程中的示例应用程序而编写的集成测试使用该博客文章中描述的技术。
摘要
这篇博客文章教会了我们两件事:
- 我们了解到,单元测试无法帮助我们验证数据访问代码是否正常运行,因为我们无法确保将正确的数据插入到数据存储中或查询返回正确的结果。
- 我们了解到,应该使用集成测试来测试数据访问代码,因为数据访问代码与使用的数据存储之间的关系是如此紧密,以至于没有必要将它们分开。
只剩下一个问题:您是否还在为数据访问代码编写单元测试?
翻译自: https://www.javacodegeeks.com/2014/07/writing-tests-for-data-access-code-unit-tests-are-waste.html