Siła pakietów w Javie

Perspektywa... ma znaczenie :)
link do repo
Package by feature, not layer

car

  • electrical
  • mechanical
  • hydraulic
  • safety
  • engine
  • steering
  • fuel system
  • ...

office

  • executives
  • managers
  • employees
  • front office
  • back office
  • accounting
  • personnel
  • mail room

Wady "typowego" podziału

  • Błędne abstrakcje
    • Trzymanie razem repozytoriów, kontrolerów itp., zachęca do szukania ich części wspólnych, a nie części wspólnych biznesu (domeny)
    • Wyższe, biznesowe abstrakcje ukryte za technicznymi
  • Łatwiej o spadek jakości
    • Klasy muszą być publiczne, więc łatwiej o brzydkie obejścia (~użyję repo bezpośrednio, potem zrefaktoruję)
  • Trudniejsze utrzymanie
    • Bugi skupiają się na funkcjach ⇒ "problem z max position", a nie "problem ze wszystkimi repozytoriami"
    • Szukanie błędu ⇒ dużo skakania między pakietami
    • Pisanie kodu ⇒ więcej klas do wyboru (wszystko public), trudniej znaleźć właściwe
➡️ TL;DR złe pakiety = potrzeba więcej dyscypliny ⬅️
Big Ball of Mud
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 refactoring
The art of destroying software (Greg Young)
Monolith vs. Microservices = big ball of mud vs. many small balls of mud
Waiting for microservices Treating packages as microservices

Szybki start - dobre praktyki z 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
                        }
                

Szybki start - dobre praktyki z 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
                        }
                

Szybki start - dobre praktyki z 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
                        }
                

Szybki start - dobre praktyki z 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
                        }
                

Szybki start - dobre praktyki z 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
                        }
                

Szybki start - dobre praktyki z 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
                        }
                

Szybki start - dobre praktyki z class w pakietach

Uncle Bob, checkstyle, Google Java Style Guide

classpackage

package-private access

S

O

L

I

D

S

O

L

I

D

S

ingle Responsibility

O

pen/Closed

L

iskov Substitution

I

nterface Segregation

D

ependency Inversion

C

omposable: plays well with others

U

nix philosophy: does one thing well

P

redictable: does what you expect

I

diomatic: feels natural

D

omain-based

C

U

P

I

D

geneza

Praca własna

  1. Zmienić ustawienia IntelliJ, żeby
    1. tworzone class, interface itp. były domyślnie package-private,
    2. dostęp pakietowy był używany domyślnie przy generowaniu metody,
    3. pokazywać ikonki dostępu w panelu Project View.
  2. Odpowiedź w moim artykule

Testowalność - BDD

  • Dan North (ten od CUPID)
  • Pakiet jako "jednostka" w teście jednostkowym
    • ~ Component Test
  • Sprawdzanie zachowań
    • Użycie klasy konfiguracyjnej do stworzenia publicznych klas w teście
    • Setup i asercje tylko przez publiczne klasy
    • Mockować tylko inne pakiety

E2E

Integration

Unit

E2E

Int

Component

Unit

Integration

Integration

Spotify: Testing of Microservices
  • Kod robi, co powinien robić
  • Działa sprawnie, w odpowiedni i przewidywalny sposób
  • Łatwość utrzymania

dokumentowanie
zachowań

spring-boot-starter-test


                    testImplementation 'org.springframework.boot:spring-boot-starter-test'
                
  • spring-boot-test, spring-test
  • JUnit (Platform + Jupiter)
  • Mockito
  • AssertJ, Hamcrest
  • Różne JSON-y, XML-e

                    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);
                      }
                    }
                

Mockito logo

  • 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);
                      }
                    }
                

Unit

  • Dobre, gdy sporo logiki w pojedynczej klasie
    • Np. walidacje w klasach z danymi
  • Zamiast mockowania - może np. Supplier?
  • Super szybkie
  • Potencjalnie do wywalenia - za blisko szczegółów implementacyjnych
Cars: I Am Speed

Component

  • Jednostką "komponent", np. cały pakiet
    • Wewnętrzne I/O (np. repozytoria) zastąpione w testach przez wersje in-memory
    • Mockowanie - tylko innych komponentów, systemów, itp.
  • Traktowanie komponentu niczym black box
  • Łatwiej potem zamienić w mikroserwis, przepisać
  • O takie BDD chodzi!
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"));
                      }
                    }
                

Integration

Lock worked in isolation Each window worked in isolation Each gate worked in isolation

Ale... Co właściwie "integrujemy"?

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
IoC = integracja między naszymi obiektami (beans)
IntelliJ showing Spring beans

coffee bean java logo

Testy integracyjne

  • Cała apka, symulacja od A do Z
  • Fallback, @HystrixCommand
  • @Transactional
    • Uwaga na transakcje samych testów
  • @Cacheable
  • @ConditionalOn...
    • ApplicationContextRunner
  • Filtry, interceptory, @PreAuthorize, AOP
@WebMvcTest
@SpringJUnitWebConfig
@DataLdapTest
@DataMongoTest
@SpringJUnitConfig
@SpringBootTest
@JsonTest
@DataJdbcTest
@DataJpaTest
@DataRedisTest
@SpringBootTest
  • Reguła kciuka: korzystać tylko z tego ;)
  • Optymalizacja testów pod cache'owanie i reużywanie kontekstów
  • Klasa bazowa, wspólna adnotacja, interfejs...

Dziękuję!

Lepsze pakiety

  • Screaming architecture, CUPID
  • Hermetyzacja, modularność
  • Łatwość usuwania
  • Bezpieczeństwo, testowalność