sit 继承测试_为什么我们不应该在测试中使用继承的三个原因

sit 继承测试

当我们为应用程序编写自动化测试(单元测试或集成测试)时,我们应该很快注意到

  1. 许多测试用例使用相同的配置来创建重复的代码。
  2. 测试中使用的构建对象会创建重复代码。
  3. 编写断言会创建重复的代码。

首先想到的是消除重复的代码。 众所周知, 请勿重复(DRY)原则指出:

每条知识都必须在系统中具有单一,明确,权威的表示形式。

因此,我们通过创建一个或多个基类来工作并删除重复的代码,该基类可配置我们的测试并为其子类提供有用的测试实用工具方法。

不幸的是,这是一个非常幼稚的解决方案 。 继续阅读,我将介绍为什么我们不应该在测试中使用继承的三个原因。

  1. 继承不是重用代码的正确工具
  2. DZone 在Misko Hevery上发表了一篇很好的访谈 ,他解释了为什么继承不是重用代码的正确工具的原因:

    继承的重点是利用多态行为而不是重用代码 ,人们错过了这一点,他们认为继承是将行为添加到类的廉价方法。 在设计代码时,我喜欢考虑选项。 当我继承时,我减少了选择。 我现在是该类的子类,不能成为其他东西的子类。 我已经将自己的构造永久固定为超类的构造,并且我对更改超类的API表示怀疑。 我的更改自由在编译时固定。

    尽管Misko Hevery在谈论编写可测​​试的代码,但我认为该规则也适用于测试。 但是在解释为什么会这样之前,让我们仔细看一下多态定义

    多态是为不同类型的实体提供单个接口。

    这就是为什么我们在测试中使用继承的原因。 我们使用继承是因为它是重用代码或配置的简便方法 。 如果我们在测试中使用继承,则意味着

  • 如果我们想确保只有相关的代码对我们的测试类可见,我们可能必须创建一个“复杂”的类层次结构,因为将所有内容都放在一个超类中并不是很“干净”。 这使得我们的测试很难阅读。
  • 我们的测试类受其父类的支配,我们对此类父类所做的任何更改都会影响其每个子类。 这使得我们的测试“难以”编写和维护。

那么,为什么这很重要呢? 这很重要,因为测试也是代码! 这就是为什么该规则也适用于测试代码的原因。

顺便说一句,您是否知道在我们的测试中使用继承的决定也会产生实际后果?

  • 继承会对测试套件的性能产生负面影响
  • 如果我们在测试中使用继承,则它会对测试套件的性能产生负面影响。 为了了解其原因,我们必须了解JUnit如何处理类层次结构:

    1. 在JUnit调用测试类的测试之前,它会查找使用@BeforeClass注释进行注释的方法。 它使用反射遍历整个类的层次结构。 到达java.lang.Object后 ,它将调用所有带有@BeforeClass注释(首先是父对象)的方法。
    2. 在JUnit调用以@Test注释进行注释的方法之前,它对以@Before注释进行注释的方法执行相同的操作
    3. JUnit执行测试后,它将查找使用@After注释进行注释的方法,并调用所有找到的方法。
    4. 执行完测试类的所有测试后,JUnit再次遍历类层次结构,并查找使用@AfterClass注释注释的方法(并调用这些方法)。

    换句话说,我们以两种方式浪费CPU时间:

    1. 测试类层次结构的遍历浪费了CPU时间。
    2. 如果我们的测试不需要设置方法,则调用设置方法和拆卸方法会浪费CPU时间。

    我从一本名为《 有效单元测试 》的书中学到了这一点。

    您可能当然会争辩说,这不是一个大问题,因为每个测试用例仅花费几毫秒。 但是,很有可能您尚未测量实际需要多长时间。

    还是你?

    例如,如果每个测试用例仅花费2毫秒,并且我们的测试套件具有3000个测试,则我们的测试套件要比实际速度慢6秒。 听起来可能不会很长时间,但是当我们在自己的计算机上运行测试时,感觉就像是永恒

    保持反馈循环尽可能快是我们的最大利益,浪费CPU时间并不能帮助我们实现该目标。

    另外,浪费的CPU时间并不是减慢反馈循环速度的唯一原因。 如果我们在测试类中使用继承,那么我们也必须付出一定的代价。

  • 使用继承使测试更难阅读
  • 自动化测试的最大好处是:

    • 测试记录了我们的代码现在的工作方式。
    • 测试可确保我们的代码正常运行。

    我们想要使我们的测试易于阅读,因为

    • 如果我们的测试易于阅读,那么很容易理解我们的代码是如何工作的。
    • 如果我们的测试易于阅读,则测试失败很容易找到问题。 如果不使用调试器就无法找出问题所在,则我们的测试还不够清楚。

    很好,但这并不能真正解释为什么使用继承会使我们的测试难以阅读。 我将通过一个简单的例子来说明我的意思。

    假设我们必须为TodoCrudServiceImpl类的create()方法编写单元测试。 TodoCrudServiceImpl类的相关部分如下所示:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    public class TodoCrudServiceImpl implements TodoCrudService {
    
        private TodoRepository repository;
       
        @Autowired
        public TodoCrudService(TodoRepository repository) {
            this.repository = repository;
        }
           
        @Transactional
        @Overrides
        public Todo create(TodoDTO todo) {
            Todo added = Todo.getBuilder(todo.getTitle())
                    .description(todo.getDescription())
                    .build();
            return repository.save(added);
        }
       
        //Other methods are omitted.
    }

    当我们开始编写此测试时,我们会记住DRY原理,并决定创建两个抽象类,以确保我们不会违反该原理。 毕竟,完成此测试后,我们必须编写其他测试,并且合理地使用尽可能多的代码是有意义的。

    首先 ,我们创建AbstractMockitoTest类。 此类确保MockitoJUnitRunner调用从其子类中找到的所有测试方法。 其源代码如下:

    import org.junit.runner.RunWith;
    import org.mockito.runners.MockitoJUnitRunner;
    
    @RunWith(MockitoJUnitRunner.class)
    public abstract class AbstractMockitoTest {
    }

    其次 ,我们创建AbstractTodoTest类。 此类为其他测试类提供了有用的实用程序方法和常量,这些其他类测试与todo条目相关的方法。 其源代码如下:

    import static org.junit.Assert.assertEquals;
    
    public abstract class AbstractTodoTest extends AbstractMockitoTest {
    
        protected static final Long ID = 1L;
        protected static final String DESCRIPTION = "description";
        protected static final String TITLE = "title";
    
        protected TodoDTO createDTO(String title, String description) {
            retun createDTO(null, title, description);
        }
    
        protected TodoDTO createDTO(Long id,
                                    String title,
                                    String description) {
            TodoDTO dto = new DTO();
           
            dto.setId(id);
            dto.setTitle(title);
            dto.setDescrption(description);
       
            return dto;
        }
       
        protected void assertTodo(Todo actual,
                                Long expectedId,
                                String expectedTitle,
                                String expectedDescription) {
            assertEquals(expectedId, actual.getId());
            assertEquals(expectedTitle, actual.getTitle());
            assertEquals(expectedDescription, actual.getDescription());
        }
    }

    现在,我们可以为TodoCrudServiceImpl类的create()方法编写单元测试。 我们的测试类的源代码如下所示:

    import org.junit.Before;
    import org.junit.Test;
    import org.mockito.Mock;
    
    import static org.mockito.Matchers.isA;
    import static org.mockito.Mockito.times;
    import static org.mockito.Mockito.verify;
    import static org.mockito.Mockito.verifyNoMoreInteractions;
    import static org.mockito.Mockito.when;
    
    public TodoCrudServiceImplTest extends AbstractTodoTest {
    
        @Mock
        private TodoRepository repositoryMock;
       
        private TodoCrudServiceImpl service;
       
        @Before
        public void setUp() {
            service = new TodoCrudServiceImpl(repository);
        }
       
        @Test
        public void create_ShouldCreateNewTodoEntryAndReturnCreatedEntry() {
            TodoDTO dto = createDTO(TITLE, DESCRIPTION);
           
            when(repositoryMock.save(isA(Todo.class))).thenAnswer(new Answer<Todo>() {
                @Override
                public Todo answer(InvocationOnMock invocationOnMock) throws Throwable {
                    Todo todo = (Todo) invocationOnMock.getArguments()[0];
                    todo.setId(ID);
                    return site;
                }
            });
                   
            Todo created = service.create(dto);
           
            verify(repositoryMock, times(1)).save(isA(Todo.class));
            verifyNoMoreInteractions(repositoryMock);
                   
            assertTodo(created, ID, TITLE, DESCRIPTION);
        }
    }

    我们的单元测试真的很容易阅读吗? 最奇怪的是,如果我们仅快速浏览一下,它看起来就很干净。 但是,当我们仔细研究它时,我们开始询问以下问题:

    • 看来TodoRepository是一个模拟对象。 此测试必须使用MockitoJUnitRunner 。 测试运行器配置在哪里?
    • 单元测试通过调用createDTO()方法创建新的TodoDTO对象。 在哪里可以找到这种方法?
    • 从此类中找到的单元测试使用常量。 这些常量在哪里声明?
    • 单元测试通过调用assertTodo()方法来声明返回的Todo对象的信息。 在哪里可以找到这种方法?

    这些似乎是“小”问题。 但是,找出这些问题的答案需要花费时间,因为我们必须阅读AbstractTodoTestAbstractMockitoTest类的源代码。

    如果我们无法通过阅读源代码来理解像这样的简单单元,那么很显然,试图理解更复杂的测试用例将非常痛苦

    更大的问题是,像这样的代码使我们的反馈循环比必要的时间长得多。

    我们应该做什么?

    我们刚刚了解了为什么我们不应该在测试中使用继承的三个原因。 显而易见的问题是:

    如果我们不应该使用继承来重用代码和配置,我们应该怎么做?

    这是一个很好的问题,我将在另一篇博客文章中回答。

    翻译自: https://www.javacodegeeks.com/2014/04/three-reasons-why-we-should-not-use-inheritance-in-our-tests.html

    sit 继承测试

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值