Mvitimin / Microservices_study

Study for MSA
0 stars 0 forks source link

이펙티브 소프트웨어 테스팅 #19

Open Mvitimin opened 6 months ago

Mvitimin commented 6 months ago

SourceCode (https://github.com/effective-software-testing/code)

6장 테스트 더블과 모의 객체

스텁

스텁은 테스트 과정에서 수행된 호출에 대해 하드 코딩된 응답을 제공한다.

모의 객체

모의객체는 스텁 그 이상의 역할을 한다. 예를 들어 getAllInvoices메서드가 한번만 호출되기 바란다면 한번만 호출됐다라는 단언문을 작성할 수 있다.

스파이

스파이는 의존성을 감시한다. 스파이는 실제 객체를 감싸서 그 행동을 관찰한다. 스파이는 특정 맥락에서 사용된다.

Mockito

의존성 스텁화

public class InvoiceFilterWithDatabase {

    public List<Invoice> lowValueInvoices() {
        DatabaseConnection connection = new DatabaseConnection();
        IssuedInvoices issuedInvoices = new IssuedInvoices(connection);

        try {
            List<Invoice> all = issuedInvoices.all();

            return all.stream()
                    .filter(invoice -> invoice.getValue() < 100)
                    .collect(toList());
        } finally {
            connection.close();
        }
    }
}

public class InvoiceFilterWithDatabaseTest {
    private IssuedInvoices invoices;
    private DatabaseConnection dbConnection;

    @BeforeEach
    public void open() {
        dbConnection = new DatabaseConnection();
        invoices = new IssuedInvoices(dbConnection);

        // we need to clean up all the tables,
        // to make sure old data doesn't interfere with the test.
        dbConnection.resetDatabase();
    }

    @AfterEach
    public void close() {
        if (dbConnection != null) dbConnection.close();
    }

    @Test
    void filterInvoices() {
        final var mauricio = new Invoice("Mauricio", 20);
        final var steve = new Invoice("Steve", 99);
        final var frank = new Invoice("Frank", 100);

        // really saves the invoice to the database...
        // this is no good.
        invoices.save(mauricio);
        invoices.save(steve);
        invoices.save(frank);

        final InvoiceFilterWithDatabase filter = new InvoiceFilterWithDatabase();

        assertThat(filter.lowValueInvoices())
                .containsExactlyInAnyOrder(mauricio, steve);
    }
}

InvoiceFilter에 대한 단위 테스트로 초점을 바꿔보자.

public class InvoiceFilter {

    private final IssuedInvoices issuedInvoices;

    public InvoiceFilter(IssuedInvoices issuedInvoices) {
        this.issuedInvoices = issuedInvoices;
    }
    public List<Invoice> lowValueInvoices() {
        List<Invoice> all = issuedInvoices.all();

        return all.stream()
                .filter(invoice -> invoice.getValue() < 100)
                .collect(toList());
    }
}

public class InvoiceFilterTest {

    @Test
    void filterInvoices() {
        IssuedInvoices issuedInvoices = mock(IssuedInvoices.class);

        Invoice mauricio = new Invoice("Mauricio", 20);
        Invoice steve = new Invoice("Steve", 99);
        Invoice frank = new Invoice("Frank", 100);

        List<Invoice> listOfInvoices = Arrays.asList(mauricio, steve, frank);
        when(issuedInvoices.all()).thenReturn(listOfInvoices);

        InvoiceFilter filter = new InvoiceFilter(issuedInvoices);

        assertThat(filter.lowValueInvoices())
                .containsExactlyInAnyOrder(mauricio, steve);
    }

    // you may want to add more tests here.
}

모의 객체와 기댓값

public interface SAP {
    void send(Invoice invoice);
}

public class SAPInvoiceSender {

    private final InvoiceFilter filter;
    private final SAP sap;

    public SAPInvoiceSender(InvoiceFilter filter, SAP sap) {
        this.filter = filter;
        this.sap = sap;
    }

    public void sendLowValuedInvoices() {
        List<Invoice> lowValuedInvoices = filter.lowValueInvoices();
        for(Invoice invoice : lowValuedInvoices) {
             sap.send(invoice);
        }
    }
}

public class SAPInvoiceSenderTest {

    private InvoiceFilter filter = mock(InvoiceFilter.class);
    private SAP sap = mock(SAP.class);
    private SAPInvoiceSender sender = new SAPInvoiceSender(filter, sap);

    @Test
    void sendToSap() {
        Invoice mauricio = new Invoice("Mauricio", 20);
        Invoice frank = new Invoice("Frank", 99);

        List<Invoice> invoices = Arrays.asList(mauricio, frank);
        when(filter.lowValueInvoices()).thenReturn(invoices);

        sender.sendLowValuedInvoices();

        verify(sap).send(mauricio);
        verify(sap).send(frank);
    }
 // send 메서드가 두 송장에 대해 호출되었는지 확인한다.

    @Test
    void noLowValueInvoices() {
        List<Invoice> invoices = emptyList();
        when(filter.lowValueInvoices()).thenReturn(invoices);

        sender.sendLowValuedInvoices();

        verify(sap, never()).send(any(Invoice.class));
    }
}

verify(sap, times(2)).send(any(Invoice.class)); // 어떤 송장에 대해 send 메서드가 정확히 두번 호출되었음을 검증한다. verify(sap, times(1)).send(mauricio); // mauricio 송장에 대해 send 메서드가 정확히 한번 호출되었음을 검증한다. verify(sap, times(1)).send(frank); // frank 송장에 대해 send 메서드가 정확히 한번 호출되었을 검증한다. verify(sap, never()).send(any(Invoice.class)); // 어떤 송장에 대해서도 send() 메서드가 호출되지 않았음을 검증한다.

인수 포획

public class SapInvoice {
    private final String customer;
    private final int value;
    private final String id;

    public SapInvoice(String customer, int value, String id) {
        assert customer!=null;
        assert id!=null;

        this.customer = customer;
        this.value = value;
        this.id = id;
    }

    public String getCustomer() {
        return customer;
    }

    public int getValue() {
        return value;
    }

    public String getId() {
        return id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        SapInvoice that = (SapInvoice) o;
        return value == that.value && customer.equals(that.customer) && id.equals(that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(customer, value, id);
    }

    @Override
    public String toString() {
        return "SapInvoice{" +
                "customer='" + customer + '\'' +
                ", value=" + value +
                ", id='" + id + '\'' +
                '}';
    }
}

public interface SAP {
    void send(SapInvoice invoice);
}

public class SAPInvoiceSender {

    private final InvoiceFilter filter;
    private final SAP sap;

    public SAPInvoiceSender(InvoiceFilter filter, SAP sap) {
        this.filter = filter;
        this.sap = sap;
    }

    public void sendLowValuedInvoices() {
        List<Invoice> lowValuedInvoices = filter.lowValueInvoices();
        for(Invoice invoice : lowValuedInvoices) {
            String customer = invoice.getCustomer();
            int value = invoice.getValue();
            String sapId = generateId(invoice);

            SapInvoice sapInvoice = new SapInvoice(customer, value, sapId);
            sap.send(sapInvoice);
        }
    }

// ID를 다음과 같은 규칙으로 생성한다.
    private String generateId(Invoice invoice) {
        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
        String customer = invoice.getCustomer();
        return date + (customer.length()>=2 ? customer.substring(0,2) : "X");
    }
}

public class SAPInvoiceSenderTest {

    private InvoiceFilter filter = mock(InvoiceFilter.class);
    private SAP sap = mock(SAP.class);
    private SAPInvoiceSender sender = new SAPInvoiceSender(filter, sap);

// 두개의 테스트 케이스를 전달한다.
    @ParameterizedTest
    @CsvSource({
            "Mauricio,Ma",
            "M,X"}
    )
    void sendToSapWithTheGeneratedId(String customer, String initialId) {
        Invoice mauricio = new Invoice(customer, 20);

        List<Invoice> invoices = Arrays.asList(mauricio);
        when(filter.lowValueInvoices()).thenReturn(invoices);

        sender.sendLowValuedInvoices();

        ArgumentCaptor<SapInvoice> captor = ArgumentCaptor.forClass(SapInvoice.class);
        verify(sap).send(captor.capture());

        SapInvoice generatedSapInvoice = captor.getValue();

        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
        assertThat(generatedSapInvoice).isEqualTo(new SapInvoice(customer, 20, date + initialId));
    }

    @Test
    void oldExample() {
        Invoice mauricio = new Invoice("Mauricio", 20);

        List<Invoice> invoices = Arrays.asList(mauricio);
        when(filter.lowValueInvoices()).thenReturn(invoices);

        sender.sendLowValuedInvoices();

        verify(sap).send(any(SapInvoice.class));
    }

}
// 리팩토링해서 풀수도 있다.
public class InvoiceToSapInvoiceConverter {

    public SapInvoice convert(Invoice invoice) {
        String customer = invoice.getCustomer();
        int value = invoice.getValue();
        String sapId = generateId(invoice);

        SapInvoice sapInvoice = new SapInvoice(customer, value, sapId);
        return sapInvoice;
    }

    private String generateId(Invoice invoice) {
        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
        String customer = invoice.getCustomer();
        return date + (customer.length()>=2 ? customer.substring(0,2) : "X");
    }
}

예외를 던지는 모의 객체

public class SAPInvoiceSenderTest {

    private InvoiceFilter filter = mock(InvoiceFilter.class);
    private SAP sap = mock(SAP.class);
    private SAPInvoiceSender sender = new SAPInvoiceSender(filter, sap);

    @Test
    void returnFailedInvoices() {
        Invoice mauricio = new Invoice("Mauricio", 20);
        Invoice frank = new Invoice("Frank", 25);
        Invoice steve = new Invoice("Steve", 48);

        List<Invoice> invoices = Arrays.asList(mauricio, frank, steve);
        when(filter.lowValueInvoices()).thenReturn(invoices);

        String date = LocalDate.now().format(DateTimeFormatter.ofPattern("MMddyyyy"));
        SapInvoice franksInvoice = new SapInvoice("Frank", 25, date + "Fr");
        doThrow(new SAPException()).when(sap).send(franksInvoice);

        List<Invoice> failedInvoices = sender.sendLowValuedInvoices();
        assertThat(failedInvoices).containsExactly(frank);

        SapInvoice mauriciosInvoice = new SapInvoice("Mauricio", 20, date + "Ma");
        verify(sap).send(mauriciosInvoice);

        SapInvoice stevesInvoice = new SapInvoice("Steve", 48, date + "St");
        verify(sap).send(stevesInvoice);

    }

}
Mvitimin commented 6 months ago

https://www.baeldung.com/mockito-series

@Mock, @Spy, @Captor and @InjectMocks

https://www.baeldung.com/mockito-annotations

Captor란 : 매개변수 인자 검증

  @Captor
    ArgumentCaptor<List<Integer>> listArgumentCaptor;    

    @Test
    void test() {
        List<Integer> numbers = new LinkedList<>();

        machine.addNumbers(numbers);

        verify(twoInserter).insertNumber(listArgumentCaptor.capture());
        List<Integer> argumentNumbers = listArgumentCaptor.getValue();
        assertEquals(1, argumentNumbers.get(0));
    }

BDD Mockito

https://www.baeldung.com/bdd-mockito

public class PhoneBookService {
    private PhoneBookRepository phoneBookRepository;

    public void register(String name, String phone) {
        if(!name.isEmpty() && !phone.isEmpty()
          && !phoneBookRepository.contains(name)) {
            phoneBookRepository.insert(name, phone);
        }
    }

    public String search(String name) {
        if(!name.isEmpty() && phoneBookRepository.contains(name)) {
            return phoneBookRepository.getPhoneNumberByContactName(name);
        }
        return null;
    }
}
// Let’s look at an example of a test body using traditional Mockito:

when(phoneBookRepository.contains(momContactName))
  .thenReturn(false);

phoneBookService.register(momContactName, momPhoneNumber);

verify(phoneBookRepository)
  .insert(momContactName, momPhoneNumber);

// Let’s see how that compares to BDDMockito:

given(phoneBookRepository.contains(momContactName))
  .willReturn(false);

phoneBookService.register(momContactName, momPhoneNumber);

then(phoneBookRepository)
  .should()
  .insert(momContactName, momPhoneNumber);

Returning a Fixed Value

// Using BDDMockito, we could easily configure Mockito to return a fixed result whenever our mock object target method is invoked:

given(phoneBookRepository.contains(momContactName))
  .willReturn(false);

phoneBookService.register(xContactName, "");

then(phoneBookRepository)
  .should(never())
  .insert(momContactName, momPhoneNumber);

Returning a Dynamic Value

given(phoneBookRepository.contains(momContactName))
  .willReturn(true);
given(phoneBookRepository.getPhoneNumberByContactName(momContactName))
  .will((InvocationOnMock invocation) ->
    invocation.getArgument(0).equals(momContactName) 
      ? momPhoneNumber 
      : null);
phoneBookService.search(momContactName);
then(phoneBookRepository)
  .should()
  .getPhoneNumberByContactName(momContactName);

Throwing an Exception

given(phoneBookRepository.contains(xContactName))
  .willReturn(false);
willThrow(new RuntimeException())
  .given(phoneBookRepository)
  .insert(any(String.class), eq(tooLongPhoneNumber));

try {
    phoneBookService.register(xContactName, tooLongPhoneNumber);
    fail("Should throw exception");
} catch (RuntimeException ex) { }

then(phoneBookRepository)
  .should(never())
  .insert(momContactName, tooLongPhoneNumber);
Mvitimin commented 6 months ago

Mockito ArgumentMatchers

https://www.baeldung.com/mockito-argument-matchers

doReturn("Flower").when(flowerService).analyze("poppy");
when(flowerService.analyze(anyString())).thenReturn("Flower");

eq 사용

// To fix this and keep the String name “poppy” as desired, we’ll use eq matcher:

when(flowerService.isABigFlower(eq("poppy"), anyInt())).thenReturn(true);

or 사용

flowerController.isAFlower("poppy");

String orMatcher = or(eq("poppy"), endsWith("y"));
assertThrows(InvalidUseOfMatchersException.class, () -> verify(flowerService).analyze(orMatcher));
Copy
The way we’d implement the above code is:

verify(flowerService).analyze(or(eq("poppy"), endsWith("y")));

Mockito ArgumentCaptor

https://www.baeldung.com/mockito-argumentcaptor

public class EmailService {

    private DeliveryPlatform platform;

    public EmailService(DeliveryPlatform platform) {
        this.platform = platform;
    }

    public void send(String to, String subject, String body, boolean html) {
        Format format = Format.TEXT_ONLY;
        if (html) {
            format = Format.HTML;
        }
        Email email = new Email(to, subject, body);
        email.setFormat(format);
        platform.deliver(email);
    }

    ...
}

@ExtendWith(MockitoExtension.class)
class EmailServiceUnitTest {

    @Mock
    DeliveryPlatform platform;

    @InjectMocks
    EmailService emailService;

    ...
}

@Test
void whenDoesSupportHtml_expectHTMLEmailFormat() {
    String to = "info@baeldung.com";
    String subject = "Using ArgumentCaptor";
    String body = "Hey, let'use ArgumentCaptor";

    emailService.send(to, subject, body, true);

    verify(platform).deliver(emailCaptor.capture());
    Email value = emailCaptor.getValue();
    assertThat(value.getFormat()).isEqualTo(Format.HTML);
}

eq와 차이

Credentials credentials = new Credentials("baeldung", "correct_password", "correct_key");
when(platform.authenticate(eq(credentials))).thenReturn(AuthenticationStatus.AUTHENTICATED);

assertTrue(emailService.authenticatedSuccessfully(credentials));

// Here we use eq(credentials) to specify when the mock should return an object.
// Next, consider the same test using an ArgumentCaptor instead:

Credentials credentials = new Credentials("baeldung", "correct_password", "correct_key");
when(platform.authenticate(credentialsCaptor.capture())).thenReturn(AuthenticationStatus.AUTHENTICATED);

assertTrue(emailService.authenticatedSuccessfully(credentials));
assertEquals(credentials, credentialsCaptor.getValue());
Mvitimin commented 6 months ago

https://www.baeldung.com/junit https://www.baeldung.com/?s=Junit

JUNIT

assertAll() vs Multiple Assertions

https://www.baeldung.com/junit5-assertall-vs-multiple-assertions

User user = new User("baeldung", "support@baeldung.com", false);
assertAll(
  "Grouped Assertions of User",
  () -> assertEquals("admin", user.getUsername(), "Username should be admin"),
  () -> assertEquals("admin@baeldung.com", user.getEmail(), "Email should be admin@baeldung.com"),
  () -> assertTrue(user.getActivated(), "User should be activated")
);

Asserting Equality on Two Classes Without an equals() Method

https://www.baeldung.com/java-assert-equality-no-equals

Person expected = new Person(1L, "Jane", "Doe");
Address address1 = new Address(1L, "New York", "Sesame Street", "United States");
expected.setAddress(address1);

Person actual = new Person(1L, "Jane", "Doe");
Address address2 = new Address(1L, "New York", "Sesame Street", "United States");
actual.setAddress(address2);

assertTrue(new ReflectionEquals(expected, "address").matches(actual));
assertTrue(new ReflectionEquals(expected.getAddress()).matches(actual.getAddress()));
Mvitimin commented 5 months ago

Assertions in JUnit 4 and JUnit 5

https://www.baeldung.com/junit-assertions

Parameterized tests using Json file

 @ParameterizedTest
    @JsonFileSource(resources ="/request.json")
    public void addClientUsingJsonInput(JsonObject object) throws Exception {
        MediaType MEDIA_TYPE_JSON_UTF8 = new MediaType("application", "json", java.nio.charset.Charset.forName("UTF-8"));
        mockMvc.perform(post("/client").contentType(MEDIA_TYPE_JSON_UTF8)
                .content(object.toString()))
                .andExpect(status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath( "$.errorMessage").doesNotExist())
                .andExpect(MockMvcResultMatchers.jsonPath("$.status").value(true))
                .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Client added successfully."))
                .andDo(print());
    }
Mvitimin commented 5 months ago

https://www.baeldung.com/java-unit-testing-best-practices

Test Case Naming Convention

The test names should be insightful, and users should understand the behavior and expectation of the test by just glancing at the name itself.

For example, the name of our unit test was testCalculateArea, which is vague on any meaningful information about the test scenario and expectation.

Therefore, we should name a test with the action and expectation such as testCalculateAreaWithGeneralDoubleValueRadiusThatReturnsAreaInDouble, testCalculateAreaWithLargeDoubleValueRadiusThatReturnsAreaAsInfinity.

However, we can still improve the names for better readability.

It’s often helpful to name the test cases in given_when_then to elaborate on the purpose of a unit test:

public class CircleTest {

    //...

    @Test
    public void givenRadius_whenCalculateArea_thenReturnArea() {
        //...
    }

    @Test
    public void givenDoubleMaxValueAsRadius_whenCalculateArea_thenReturnAreaAsInfinity() {
        //...
    }
}
Mvitimin commented 5 months ago

Parameterized Testing

https://www.baeldung.com/parameterized-tests-junit-5

static Stream<Arguments> arguments = Stream.of(
  Arguments.of(null, true), // null strings should be considered blank
  Arguments.of("", true),
  Arguments.of("  ", true),
  Arguments.of("not blank", false)
);

@ParameterizedTest
@VariableSource("arguments")
void isBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource(
  String input, boolean expected) {
    assertEquals(expected, Strings.isBlank(input));
}
Mvitimin commented 5 months ago

Answer 이용

@Test
void givenValidUser_whenSaveUser_thenSucceed(@Mock MailClient mailClient) {
    // Given
    user = new User("Jerry", 12);
    when(userRepository.insert(any(User.class))).then(new Answer<User>() {
        int sequence = 1;

        @Override
        public User answer(InvocationOnMock invocation) throws Throwable {
            User user = (User) invocation.getArgument(0);
            user.setId(sequence++);
            return user;
        }
    });

    userService = new DefaultUserService(userRepository, settingRepository, mailClient);

    // When
    User insertedUser = userService.register(user);

    // Then
    verify(userRepository).insert(user);
    assertNotNull(user.getId());
    verify(mailClient).sendUserRegistrationMail(insertedUser);
}
Mvitimin commented 5 months ago

Mocking Private Fields With

https://www.baeldung.com/java-mockito-private-fields

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class MockServiceUnitTest {
    private Person mockedPerson;

    @BeforeEach
    public void setUp(){
        mockedPerson = mock(Person.class);
    }
}

@Test
void givenNameChangedWithReflectionUtils_whenGetName_thenReturnName() throws Exception {
    MockService mockService = new MockService();
    Field field = ReflectionUtils
      .findFields(MockService.class, f -> f.getName().equals("person"),
        ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
      .get(0);

    field.setAccessible(true);
    field.set(mockService, mockedPerson);

    when(mockedPerson.getName()).thenReturn("Jane Doe");

    Assertions.assertEquals("Jane Doe", mockService.getName());
}
Mvitimin commented 5 months ago

Mocking a Singleton With Mockito

https://www.baeldung.com/java-mockito-singleton


@Test
void givenValueExistsInCache_whenGetProduct_thenDAOIsNotCalled_mockingStatic() {
    ProductDAO productDAO = mock(ProductDAO.class);
    CacheManager cacheManager = mock(CacheManager.class);
    Product product = new Product("product1", "description");

    try (MockedStatic<CacheManager> cacheManagerMock = mockStatic(CacheManager.class)) {
        cacheManagerMock.when(CacheManager::getInstance).thenReturn(cacheManager);
        when(cacheManager.getValue(any(), any())).thenReturn(product);

        ProductService productService = new ProductService(productDAO);
        productService.getProduct("product1");

        Mockito.verify(productDAO, times(0)).getProduct(any());
    }
}
Mvitimin commented 5 months ago

Testing an Abstract Class With JUnit

https://www.baeldung.com/junit-test-abstract-class

public abstract class AbstractMethodCalling {

    public abstract String abstractFunc();

    public String defaultImpl() {
        String res = abstractFunc();
        return (res == null) ? "Default" : (res + " Default");
    }
}

@Test
public void givenDefaultImpl_whenMockAbstractFunc_thenExpectedBehaviour() {
    AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class);
    Mockito.when(cls.abstractFunc())
      .thenReturn("Abstract");
    Mockito.doCallRealMethod()
      .when(cls)
      .defaultImpl();

    assertEquals("Abstract Default", cls.defaultImpl());
}