Практика модульного тестирования Spring Boot

модульный тест
Практика модульного тестирования Spring Boot

В последнее время я много обдумывал и писал юнит-тесты, ссылался на множество статей про юнит-тестирование Spring Boot, но большинство кейсов прошло@SpringBootTestДля запуска одиночного теста после многих тренировок я обнаружил, что это не оптимальное решение для модульного тестирования, этот метод больше подходит для интеграционного тестирования.

@RunWith(SpringRunner.class)
@SpringBootTest
public class FooServiceTest {

    @Autowired
    private FooService service;

    @Test
    public void get() {
        FooVO foo = service.get("id");
        assertThat(foo).isNotNull();
    }

}

1. Модульное тестирование

в общем

Принципы ВОЗДУХА

  • Automatic(Автоматизированный): результат выполнения автоматически выдается с помощью серии утверждений без человеческого суждения Трудно судить вручную по десяткам или сотням тестовых случаев.
  • Independent(Независимый): тестовые случаи не могут зависеть друг от друга и являются независимыми.
  • Repeatable(Повторяемый): Модульные тесты могут выполняться повторно, и на них не может влиять внешняя среда.Внешние зависимости, такие как базы данных, удаленные вызовы и промежуточное ПО, не могут влиять на выполнение тестовых случаев.

Тестируемость

В настоящее время в реальной разработке юнит-тестирование — это скорее проверка методов (функций), а самое главное для методов — этометод должен быть измеримым, если метод не поддается тестированию или его трудно протестировать, это означает, что существует проблема со структурой этого метода и его необходимо скорректировать.Видно, что модульное тестирование полезно для проектирования структуры кода.

выгода

Без юнит-тестирования логическая модификация или рефакторинг этого метода в будущем будет стоить дороже, потому что нет быстрых, эффективных и надежных средств, гарантирующих, что измененные результаты верны и не повлияют на другую бизнес-логику

При наличии достаточного количества тестовых случаев для этого метода последующие логические модификации или корректировки рефакторинга могут быть выполнены с уверенностью, поскольку модульные тесты могут использоваться для проверки того, соответствуют ли скорректированные и рефакторинговые результаты ожиданиям.

недостаточный

Стоимость написания юнит-тестов слишком высока, особенно когда задача разработки срочная, что является одной из причин сложности реализации TDD.

2. Родственные понятия

аннотация

@RunWith(SpringRunner.class)

Указывает, что тестовый пример выполняется в тестовой среде Spring,SpringRunnerдаSpringJUnit4ClassRunnerПсевдоним для этого Runner, который предоставляет среду контейнера Spring.

@RunWith: Когда класс аннотируется с @Runwith или расширяет класс, аннотированный с @Runwith, JUnit будет вызывать ссылки на его классу, чтобы запустить тесты в этом классе вместо Runner, встроенном в Junit.

SpringJUnit4ClassRunner: SpringJUnit4ClassRunner — это пользовательское расширение BlockJUnit4ClassRunner JUnit, которое обеспечивает функциональность Spring TestContext Framework для стандартных тестов JUnit с помощью TestContextManager и связанных классов поддержки и аннотаций.

@SpringBootTest

Эта аннотация используется для запуска реального контейнера Spring для тестирования с загруженнымApplicationContext, поэтому вы можете вводить и использовать bean-компоненты в контейнере Spring по своему усмотрению, как показано ниже.image.pngОднако фактическая служба будет зависеть от различных внешних служб, таких как база данных, Redis, MQ и т. д. В этой аннотации также необходимо настроить соответствующую информацию для выполнения модульного теста после обычного запуска, что нарушает модульный тест.Повторяемыйв общем

Вводя эти зависимости, объем обслуживания станет большим, что приведет к долгому запуску, особенно если на машине недостаточно памяти, ей может действительно понадобиться простой отладчик. Тестовые случаи, особенно когда этот тестовый пример требует частого ввода в эксплуатацию, вы нужно ждать сервис медленно и долго, а потом выполнять тест кейсы, надо сказать что это способ неэффективный В данном примере не будем упоминать об этом в арте, ведь Демо не нужно полагаться на внешние сервисы и промежуточного программного обеспечения и останавливается за считанные секунды.

модульное тестированиеRНужно следовать принципу, и не стоит полагаться на внешние сервисы и промежуточное ПО, ведь в большинстве случаев на этапе юнит-тестирования такого промежуточного ПО нет, особенно в процессе CI/CD.

@MockBean

spring-boot-testАннотации, предоставленные пакетом, используются для имитации некоторых bean-компонентов в контейнере Spring. Когда цель теста зависит от базовых bean-компонентов, вы можете имитировать внедрение через аннотации, чтобы избежать фактического вызова bean-компонентов.В конце концов, может и не быть настоящего базу данных или другие внешние зависимости.

@Service
public class FooService {
    @Autowired
    private FooRepository fooRepository;
    @Autowired
    private BarRepository barRepository;
    
    // some method
}

@RunWith(SpringRunner.class)
public class FooServiceTest {
    @Autowired
    private FooService fooService;
    @MockBean
    private FooRepository fooRepository;
    @MockBean
    private BarRepository barRepository;
    
    // some test method
}

@Import 

обеспечитьбыстрыйДобавьте Bean в контейнер Spring, чтобы Bean можно было внедрить (эта аннотация, похоже, не имеет ничего общего с модульным тестированием, на самом деле это не имеет значения, но ее также можно использовать в модульном тестировании)

// 快速导入
@Import({FooService.class})
@RunWith(SpringRunner.class)
public class BarServiceTest extends BaseTest {
    @Autowired
    private FooService fooService;

    @MockBean
    private FooRepository fooRepository;
    @MockBean
    private BarRepository barRepository;
 	// some test method
}

// 正常使用
@RunWith(SpringRunner.class)
public class OtherServiceTest extends BaseTest {
    @Autowired
    private FooService fooService;

    @MockBean
    private FooRepository fooRepository;
    @MockBean
    private BarRepository barRepository;

    // 提供一些测试相关的配置入口,也仅限于 test,ComponentScan 会跳过此类的
    @TestConfiguration
    static class TestContextConfiguration {
        @Bean
        public FooService fooService() {
            return new FooService();
        }
    }
 	// some test method
}

Mockito

Очень важной идеей одиночного тестирования является Mock. С помощью Mock этого можно достичь, не полагаясь на какие-либо внешние сервисы или промежуточное ПО, и сосредоточившись только на логике самого метода. Любые внешние зависимости должны быть завершены с помощью Mock. Если нет возможности сделать Mock, то репрезентативные методы и классы неизмеримы, есть проблема со структурой кода и требуется структурная корректировка

Mockito — относительно мощный фреймворк Mock на Java, в Интернете есть много статей об использовании этого фреймворка, поэтому я его пропущу.

Научить вас, как использовать Mockito

Инструмент модульного тестирования Mockito framework

Mockito API Docs

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn't give you hangover because the tests are very readable and they produce clean verification errors.

Имитация заполнения данных

Создайте настоящие фиктивные данные, два типа перечислены ниже, в зависимости от личных предпочтений, просто выберите один

jfairy

GitHub.com/code Arte/похудение…

@Test
public void name() {
    Fairy fairy = Fairy.create();
    fairy.person();
    fairy.company();
    fairy.creditCard();
    fairy.textProducer();
    fairy.baseProducer();	
    fairy.dateProducer();
    fairy.networkProducer();
}

java-faker

GitHub.com/DI США/Java-Fa…

@Test
public void name() {
    Faker faker = new Faker();
    String name = faker.name().fullName(); // Miss Samanta Schmidt
    String firstName = faker.name().firstName(); // Emory
    String lastName = faker.name().lastName(); // Barton
    String streetAddress = faker.address().streetAddress(); // 60018 Sawayn Brooks Suite 449
}

утверждение

Определить, соответствует ли результат выполнения метода ожиданиям

Модульные тесты бессмысленны без утверждений, каждый раз, когда логика кода изменяется, обратная связь может быть вовремя получена через утверждения, поэтому утверждения необходимы.

3. Примеры

// FooService
public FooVO get(String id) {
    BarDO barDO = barRepository.getByFooId(id);
    FooDO fooDO = fooRepository.get(id);
    FooVO foo = new FooVO();
    foo.setStatus(barDO.getType());
    foo.setId(id);
    foo.setName(fooDO.getName() + "-suffix");
    switch (barDO.getType()) {
        case FAILED:
            foo.setContent("this is failed result");
            break;
        case SUCCESS:
            foo.setContent("this is success result");
            break;
        default:
            throw new DummyException("some exception happened!");
    }
    return foo;
}

нормальный поток

// BarServiceTest
@Test
public void getSuccess() {
    String name = getName();
    when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.SUCCESS));
    when(fooRepository.get(ID)).thenReturn(mockFoo(name));
    FooVO fooVO = fooService.get(ID);
    assertThat(fooVO).isNotNull();
    assertThat(fooVO.getContent()).isEqualTo("this is success result");
    assertThat(fooVO.getName()).isEqualTo(name + "-suffix");
}

image.pngКогда логика немного изменена и запущены модульные тесты, утверждение не выполняется.

foo.setContent("this is success result...");

image.png

поток исключений

// BarServiceTest
@Rule
public ExpectedException expected = ExpectedException.none();
@Test
public void getException() {
    expected.expect(DummyException.class);
    expected.expectMessage("exception happened"); // 包含
    when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.EXCEPTION));
    when(fooRepository.get(ID)).thenReturn(mockFoo(getName()));
    fooService.get(ID);
}

Полное дело

@Import({FooService.class})
@RunWith(SpringRunner.class)
public class BarServiceTest extends BaseTest {
    @Autowired
    private FooService fooService;

    @MockBean
    private FooRepository fooRepository;
    @MockBean
    private BarRepository barRepository;
    
    @Rule
    public ExpectedException expected = ExpectedException.none();

    // ignore setUp/tearDown
    
    @Test
    public void getSuccess() {
        String name = getName();
        when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.SUCCESS));
        when(fooRepository.get(ID)).thenReturn(mockFoo(name));
        FooVO fooVO = fooService.get(ID);
        assertThat(fooVO).isNotNull();
        assertThat(fooVO.getContent()).isEqualTo("this is success result");
        assertThat(fooVO.getName()).isEqualTo(name + "-suffix");
    }

    @Test
    public void getFailed() {
        String name = getName();
        when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.FAILED));
        when(fooRepository.get(ID)).thenReturn(mockFoo(name));
        FooVO fooVO = fooService.get(ID);
        assertThat(fooVO).isNotNull();
        assertThat(fooVO.getContent()).isEqualTo("this is failed result");
        assertThat(fooVO.getName()).isEqualTo(name + "-suffix");
    }

    @Test
    public void getException() {
        expected.expect(DummyException.class);
        expected.expectMessage("exception happened");
        when(barRepository.getByFooId(ID)).thenReturn(mockBar(Status.EXCEPTION));
        when(fooRepository.get(ID)).thenReturn(mockFoo(getName()));
        fooService.get(ID);
    }

}

@Service
public class FooService {
    @Autowired
    private FooRepository fooRepository;
    @Autowired
    private BarRepository barRepository;

    // 当此方法的逻辑有任何的调整,测试用例都有可能执行失败
    public FooVO get(String id) {
        BarDO barDO = barRepository.getByFooId(id);
        FooDO fooDO = fooRepository.get(id);
        FooVO foo = new FooVO();
        foo.setStatus(barDO.getType());
        foo.setId(id);
        foo.setName(fooDO.getName() + "-suffix");
        switch (barDO.getType()) {
            case FAILED:
                foo.setContent("this is failed result");
                break;
            case SUCCESS:
                foo.setContent("this is success result");
                break;
            default:
                throw new DummyException("some exception happened!");
        }
        return foo;
    }
}

подробный код,портал

Образцы основаны на Junit4, Junit5 на основе 4, оптимизированы для улучшения много, но это не самое главное, удобный в использовании инструмент опять же, если метод непредсказуем, с делами все же утверждают, что хороший дизайн не напрасно

Другие концепции, такие как @Rule и @TestConfiguration, будут обсуждаться позже, когда у них будет возможность попрактиковаться.

4. Вывод

Agile Manifesto говорит, чтореагировать на изменения, а для разработчиков модульное тестирование — это эффективный и надежный способ реагирования на изменения требований или других уровней, а любые изменения в логике кода могут быстро получить обратную связь в тестовых примерах.