public
),
trudniej znaleźć właściwe
Even systems with well-defined architectures are prone to structural erosion. The relentless onslaught of changing requirements (...) can gradually undermine its structure
There are good reasons that good programmers build BIG BALLS OF MUD. It may well be that (...) the market moves so fast that (...) expedient, slash-and-burn, disposable programming is, in fact, a state-of-the-art strategy
Therefore, if you can’t easily make a mess go away, at least cordon it off. This restricts the disorder to a fixed area, keeps it out of sight, and can set the stage for additional refactoringThe art of destroying software (Greg Young)
class
w pakietach
public class Encapsulation {
public static final int CONSTANT = 1_077;
protected final int hierarchyProperty;
private String internalProperty;
protected Encapsulation(int hierarchyProperty) {
this.hierarchyProperty = hierarchyProperty + CONSTANT;
}
public void command(String value) {
overrideName(value, true);
}
public String query() {
return internalProperty;
}
private void overrideName(String value, boolean enforced) {
// ...
}
}
class HiddenHelper {
// similar to above
}
class
w pakietach
public class Encapsulation { // ~ pakiet
public static final int CONSTANT = 1_077;
protected final int hierarchyProperty;
private String internalProperty;
protected Encapsulation(int hierarchyProperty) {
this.hierarchyProperty = hierarchyProperty + CONSTANT;
}
public void command(String value) {
overrideName(value, true);
}
public String query() {
return internalProperty;
}
private void overrideName(String value, boolean enforced) {
// ...
}
}
class HiddenHelper {
// similar to above
}
class
w pakietach
public class Encapsulation { // ~ pakiet
public static final int CONSTANT = 1_077; // kontrakty (DTO)
protected final int hierarchyProperty;
private String internalProperty;
protected Encapsulation(int hierarchyProperty) {
this.hierarchyProperty = hierarchyProperty + CONSTANT;
}
public void command(String value) {
overrideName(value, true);
}
public String query() {
return internalProperty;
}
private void overrideName(String value, boolean enforced) {
// ...
}
}
class HiddenHelper {
// similar to above
}
class
w pakietach
public class Encapsulation { // ~ pakiet
public static final int CONSTANT = 1_077; // kontrakty (DTO)
protected final int hierarchyProperty;
private String internalProperty;
// ~ Spring @Configuration
protected Encapsulation(int hierarchyProperty) {
this.hierarchyProperty = hierarchyProperty + CONSTANT;
}
public void command(String value) {
overrideName(value, true);
}
public String query() {
return internalProperty;
}
private void overrideName(String value, boolean enforced) {
// ...
}
}
class HiddenHelper {
// similar to above
}
class
w pakietach
public class Encapsulation { // ~ pakiet
public static final int CONSTANT = 1_077; // kontrakty (DTO)
protected final int hierarchyProperty;
private String internalProperty;
// ~ Spring @Configuration
protected Encapsulation(int hierarchyProperty) {
this.hierarchyProperty = hierarchyProperty + CONSTANT;
}
// jedyne klasy publiczne: Facade/Service/Command + Query
public void command(String value) {
overrideName(value, true);
}
public String query() {
return internalProperty;
}
private void overrideName(String value, boolean enforced) {
// ...
}
}
class HiddenHelper {
// similar to above
}
class
w pakietach
public class Encapsulation { // ~ pakiet
public static final int CONSTANT = 1_077; // kontrakty (DTO)
protected final int hierarchyProperty;
private String internalProperty;
// ~ Spring @Configuration
protected Encapsulation(int hierarchyProperty) {
this.hierarchyProperty = hierarchyProperty + CONSTANT;
}
// jedyne klasy publiczne: Facade/Service/Command + Query
public void command(String value) {
overrideName(value, true);
}
public String query() {
return internalProperty;
}
// klasy pakietowe (package-private)
private void overrideName(String value, boolean enforced) {
// ...
}
}
class HiddenHelper {
// similar to above
}
class
w pakietachclass
⇒ package
class
, interface
itp. były domyślnie
package-private
,
testImplementation 'org.springframework.boot:spring-boot-starter-test'
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.limiting.api.AccountId;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.Clock;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LimitingFacadeTest {
private final Clock clock = mock(Clock.class);
private final AccountRepository accountRepository = mock(AccountRepository.class);
private final AccountSettingRepository accountSettingRepository = mock(AccountSettingRepository.class);
private final ReportingFacade reporting = mock(ReportingFacade.class);
private LimitingFacade systemUnderTest;
@BeforeEach
void setUp() {
systemUnderTest = new LimitingFacade(clock, accountRepository, accountSettingRepository, reporting);
}
@Test
void newAccount_overrideLimit_createsAccount() {
// given
var accountId = AccountId.valueOf("1");
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.empty());
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
var captor = ArgumentCaptor.forClass(AccountSetting.class);
verify(accountSettingRepository).save(captor.capture());
var account = captor.getValue();
assertThat(account.id()).isEqualTo(accountId);
assertThat(account.limit()).isEqualTo(1);
}
@Test
void existingAccount_overrideLimit_updatesAccount() {
// given
var accountId = AccountId.valueOf("1");
var existingAccount = mock(AccountSetting.class);
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.of(existingAccount));
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
verify(existingAccount).overrideLimit(1);
verify(accountSettingRepository).save(existingAccount);
}
}
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.limiting.api.AccountId;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.Clock;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LimitingFacadeTest {
private final Clock clock = mock(Clock.class);
private final AccountRepository accountRepository = mock(AccountRepository.class);
private final AccountSettingRepository accountSettingRepository = mock(AccountSettingRepository.class);
private final ReportingFacade reporting = mock(ReportingFacade.class);
private final LimitingFacade systemUnderTest =
new LimitingFacade(clock, accountRepository, accountSettingRepository, reporting);
@Test
void newAccount_overrideLimit_createsAccount() {
// given
var accountId = AccountId.valueOf("1");
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.empty());
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
var captor = ArgumentCaptor.forClass(AccountSetting.class);
verify(accountSettingRepository).save(captor.capture());
var account = captor.getValue();
assertThat(account.id()).isEqualTo(accountId);
assertThat(account.limit()).isEqualTo(1);
}
@Test
void existingAccount_overrideLimit_updatesAccount() {
// given
var accountId = AccountId.valueOf("1");
var existingAccount = mock(AccountSetting.class);
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.of(existingAccount));
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
verify(existingAccount).overrideLimit(1);
verify(accountSettingRepository).save(existingAccount);
}
}
- Do not mock types you don't own
- Don't mock value objects
- Don't mock everything
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.limiting.api.AccountId;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.Clock;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LimitingFacadeTest {
private final Clock clock = mock(Clock.class);
private final AccountRepository accountRepository = mock(AccountRepository.class);
private final AccountSettingRepository accountSettingRepository = mock(AccountSettingRepository.class);
private final ReportingFacade reporting = mock(ReportingFacade.class);
private final LimitingFacade systemUnderTest =
new LimitingFacade(clock, accountRepository, accountSettingRepository, reporting);
@Test
void newAccount_overrideLimit_createsAccount() {
// given
var accountId = AccountId.valueOf("1");
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.empty());
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
var captor = ArgumentCaptor.forClass(AccountSetting.class);
verify(accountSettingRepository).save(captor.capture());
var account = captor.getValue();
assertThat(account.id()).isEqualTo(accountId);
assertThat(account.limit()).isEqualTo(1);
}
@Test
void existingAccount_overrideLimit_updatesAccount() {
// given
var accountId = AccountId.valueOf("1");
var existingAccount = mock(AccountSetting.class);
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.of(existingAccount));
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
verify(existingAccount).overrideLimit(1);
verify(accountSettingRepository).save(existingAccount);
}
}
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.limiting.api.AccountId;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import java.time.Clock;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LimitingFacadeTest {
private final Clock clock = mock(Clock.class);
private final AccountRepository accountRepository = mock(AccountRepository.class);
private final AccountSettingRepository accountSettingRepository = mock(AccountSettingRepository.class);
private final ReportingFacade reporting = mock(ReportingFacade.class);
private final LimitingFacade systemUnderTest =
new LimitingFacade(clock, accountRepository, accountSettingRepository, reporting);
@Test
void newAccount_overrideLimit_createsAccount() {
// given
var accountId = AccountId.valueOf("1");
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.empty());
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
var captor = ArgumentCaptor.forClass(AccountSetting.class);
verify(accountSettingRepository).save(captor.capture());
var account = captor.getValue();
assertThat(account.id()).isEqualTo(accountId);
assertThat(account.limit()).isEqualTo(1);
}
@Test
void existingAccount_overrideLimit_updatesAccount() {
// given
var accountId = AccountId.valueOf("1");
var existingAccount = new AccountSetting(accountId.getId(), 2, 1);
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.of(existingAccount));
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
assertThat(existingAccount.limit()).isEqualTo(1);
verify(accountSettingRepository).save(existingAccount);
}
}
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.limiting.api.AccountId;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Clock;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LimitingFacadeTest {
@Mock
private Clock clock;
@Mock
private AccountRepository accountRepository;
@Mock
private AccountSettingRepository accountSettingRepository;
@Mock
private ReportingFacade reporting;
@InjectMocks
private LimitingFacade systemUnderTest;
@Captor
private ArgumentCaptor<AccountSetting> captor;
@Test
void newAccount_overrideLimit_createsAccount() {
// given
var accountId = AccountId.valueOf("1");
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.empty());
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
verify(accountSettingRepository).save(captor.capture());
var account = captor.getValue();
assertThat(account.id()).isEqualTo(accountId);
assertThat(account.limit()).isEqualTo(1);
}
@Test
void existingAccount_overrideLimit_updatesAccount() {
// given
var accountId = AccountId.valueOf("1");
var existingAccount = new AccountSetting(accountId.getId(), 2, 1);
when(accountSettingRepository.findById(accountId))
.thenReturn(Optional.of(existingAccount));
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
// then
assertThat(existingAccount.limit()).isEqualTo(1);
verify(accountSettingRepository).save(existingAccount);
}
}
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.limiting.api.AccountId;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import org.assertj.core.api.BDDAssertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Clock;
import java.util.Optional;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@ExtendWith(MockitoExtension.class)
class LimitingFacadeTest {
@Mock
private Clock clock;
@Mock
private AccountRepository accountRepository;
@Mock
private AccountSettingRepository accountSettingRepository;
@Mock
private ReportingFacade reporting;
@InjectMocks
private LimitingFacade systemUnderTest;
@Captor
private ArgumentCaptor<AccountSetting> accountSettingCaptor;
@Test
void newAccount_overrideLimit_createsAccount() {
var accountId = AccountId.valueOf("1");
given(accountSettingRepository.findById(accountId))
.willReturn(Optional.empty());
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
then(accountSettingRepository).should().save(accountSettingCaptor.capture());
var account = accountSettingCaptor.getValue();
BDDAssertions.then(account.id()).isEqualTo(accountId);
BDDAssertions.then(account.limit()).isEqualTo(1);
}
@Test
void existingAccount_overrideLimit_updatesAccount() {
var accountId = AccountId.valueOf("1");
var existingAccount = new AccountSetting(accountId.getId(), 2, 1);
given(accountSettingRepository.findById(accountId)).willReturn(Optional.of(existingAccount));
// when
systemUnderTest.overrideAccountLimit(accountId, 1);
then(accountSettingRepository).should().save(existingAccount);
BDDAssertions.then(existingAccount.limit()).isEqualTo(1);
}
}
Supplier
?
main/.../limiting
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Clock;
@Configuration
@RequiredArgsConstructor
class LimitingConfiguration {
/* just IO dependencies and other modules */
private final Clock clock;
private final AccountRepository accountRepository;
private final AccountSettingRepository accountSettingRepository;
private final ReportingFacade reportingFacade;
@Bean
LimitingFacade facade() {
return new LimitingFacade(clock, accountRepository, accountSettingRepository, reportingFacade);
}
}
testFixtures/.../limiting
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.reporting.ReportingFacade;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
class LimitingTestSetup {
private final LimitingConfiguration creator; // Spring
LimitingTestSetup(ReportingFacade reportingFacade) {
this(Clock.fixed(Instant.EPOCH, ZoneOffset.UTC), reportingFacade);
}
LimitingTestSetup(Clock clock, ReportingFacade reportingFacade) {
var accountRepository = new InMemoryAccountRepository();
var settingsRepository
= new InMemoryAccountSettingRepository(clock, accountRepository);
creator = new LimitingConfiguration(
clock, accountRepository, settingsRepository, reportingFacade);
}
LimitingFacade facade() {
return creator.facade();
}
}
test/.../limiting
package io.github.mat3e.downloads.limiting;
import io.github.mat3e.downloads.exceptionhandling.BusinessException;
import io.github.mat3e.downloads.limiting.LimitingFacade.AccountLimitExceeded;
import io.github.mat3e.downloads.limiting.api.AccountId;
import io.github.mat3e.downloads.limiting.api.Asset;
import io.github.mat3e.downloads.reporting.CapturingReportingFacade;
import org.junit.jupiter.api.Test;
import static io.github.mat3e.downloads.limiting.BusinessAssertions.then;
import static io.github.mat3e.downloads.limiting.BusinessAssertions.thenFoundIn;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.catchException;
import static org.assertj.core.api.Assertions.tuple;
class LimitingTest {
private static final AccountId ACCOUNT_ID = AccountId.valueOf("1");
private final CapturingReportingFacade reporting
= new CapturingReportingFacade();
private final LimitingTestSetup setup
= new LimitingTestSetup(reporting);
private final LimitingFacade limiting
= setup.facade();
@Test
void findAccount_unknownAccount_returnsEmpty() {
assertThat(limiting.findForAccount(ACCOUNT_ID)).isEmpty();
}
@Test
void downloadStarted_notExistingAccount_throwsNotFound() {
assertThatExceptionOfType(BusinessException.class)
.isThrownBy(() -> limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("US")))
.withMessageContaining("not found")
.withMessageContaining(ACCOUNT_ID.getId());
}
@Test
void assetRemoved_notExistingAccount_throwsNotFound() {
assertThatExceptionOfType(BusinessException.class)
.isThrownBy(() -> limiting.removeDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("US")))
.withMessageContaining("not found")
.withMessageContaining(ACCOUNT_ID.getId());
}
@Test
void overrideLimit_illegalValue_throws() {
assertThatExceptionOfType(BusinessException.class)
.isThrownBy(() -> limiting.overrideAccountLimit(ACCOUNT_ID, -1))
.withMessageContaining("negative");
}
@Test
void downloadStarted_limitNotExceeded_storesAsset() {
// given
limiting.overrideAccountLimit(ACCOUNT_ID, 1);
// when
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("US"));
thenFoundIn(limiting, ACCOUNT_ID).containsExactly(Asset.withId("123").inCountry("US"));
}
@Test
void downloadStarted_limitExceeded_doesNotStoreAsset() {
// given
limiting.overrideAccountLimit(ACCOUNT_ID, 1);
// and
limiting.assignDownloadedAsset(
ACCOUNT_ID,
Asset.withId("123").inCountry("US")
);
// when
var exception =
catchException(() -> limiting.assignDownloadedAsset(
ACCOUNT_ID,
Asset.withId("456").inCountry("US"))
);
then(exception).isInstanceOf(AccountLimitExceeded.class);
thenFoundIn(limiting, ACCOUNT_ID)
.containsExactly(Asset.withId("123").inCountry("US"));
}
@Test
void downloadStarted_sameAsset_doesNotStoreAsset() {
// given
limiting.overrideAccountLimit(ACCOUNT_ID, 2);
// and
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("US"));
// when
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("US"));
thenFoundIn(limiting, ACCOUNT_ID).containsExactly(Asset.withId("123").inCountry("US"));
reporting.recordedEvents()
.extracting("accountId", "asset")
.containsExactly(tuple(ACCOUNT_ID, Asset.withId("123").inCountry("US")));
}
@Test
void downloadStarted_sameAssetDifferentCountry_storesAsset() {
// given
limiting.overrideAccountLimit(ACCOUNT_ID, 2);
// and
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("DE"));
// when
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("FR"));
thenFoundIn(limiting, ACCOUNT_ID).containsExactly(
Asset.withId("123").inCountry("DE"),
Asset.withId("123").inCountry("FR"));
reporting.recordedEvents()
.extracting("accountId", "asset", "existingAssetCountry")
.containsExactly((tuple(ACCOUNT_ID, Asset.withId("123").inCountry("FR"), "DE")));
}
@Test
void assetRemoved_noAsset_ignores() {
// given
limiting.overrideAccountLimit(ACCOUNT_ID, 2);
// when
limiting.removeDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("US"));
thenFoundIn(limiting, ACCOUNT_ID).isEmpty();
reporting.recordedEvents()
.extracting("accountId", "asset")
.containsExactly(tuple(ACCOUNT_ID, Asset.withId("123").inCountry("US")));
}
@Test
void assetRemoved_newDownloadStarted_storesAsset() {
// given
limiting.overrideAccountLimit(ACCOUNT_ID, 2);
// and
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("DE"));
// and
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("FR"));
// when
limiting.removeDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("FR"));
// and
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("456").inCountry("DE"));
thenFoundIn(limiting, ACCOUNT_ID).containsExactly(
Asset.withId("123").inCountry("DE"),
Asset.withId("456").inCountry("DE"));
}
@Test
void downloadStarted_limitIncreased_storesAsset() {
// given
limiting.overrideAccountLimit(ACCOUNT_ID, 1);
// and
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("123").inCountry("DE"));
// when
var exception =
catchException(() -> limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("456").inCountry("DE")));
then(exception).isInstanceOf(AccountLimitExceeded.class);
thenFoundIn(limiting, ACCOUNT_ID).containsExactly(Asset.withId("123").inCountry("DE"));
// when
limiting.overrideAccountLimit(ACCOUNT_ID, 2);
// and
limiting.assignDownloadedAsset(ACCOUNT_ID, Asset.withId("456").inCountry("DE"));
thenFoundIn(limiting, ACCOUNT_ID).containsExactly(
Asset.withId("123").inCountry("DE"),
Asset.withId("456").inCountry("DE"));
}
}
A key element of Spring is infrastructural support at the application level: Spring focuses on the "plumbing" of enterprise applications Spring (i inne kontenery IoC)
[Integration Testing] lets you test the correct wiring of your Spring IoC container contexts Dokumentacja
@HystrixCommand
@Transactional
@Cacheable
@ConditionalOn...
ApplicationContextRunner
@PreAuthorize
, AOP@WebMvcTest
@SpringJUnitWebConfig
@DataLdapTest
@DataMongoTest
@SpringJUnitConfig
@SpringBootTest
@JsonTest
@DataJdbcTest
@DataJpaTest
@DataRedisTest
@SpringBootTest