В последнее время я много обдумывал и писал юнит-тесты, ссылался на множество статей про юнит-тестирование 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 по своему усмотрению, как показано ниже.Однако фактическая служба будет зависеть от различных внешних служб, таких как база данных, 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 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
@Test
public void name() {
Fairy fairy = Fairy.create();
fairy.person();
fairy.company();
fairy.creditCard();
fairy.textProducer();
fairy.baseProducer();
fairy.dateProducer();
fairy.networkProducer();
}
java-faker
@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");
}
Когда логика немного изменена и запущены модульные тесты, утверждение не выполняется.
foo.setContent("this is success result...");
поток исключений
// 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 говорит, чтореагировать на изменения, а для разработчиков модульное тестирование — это эффективный и надежный способ реагирования на изменения требований или других уровней, а любые изменения в логике кода могут быстро получить обратную связь в тестовых примерах.