Problem z testami jednostkowymi

0

Witam

aktualnie wlaczę z testami i będę wdzięczny za informacje co robi nie tak.

Mam prosty model danych:

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "contractors")
@Inheritance(strategy = InheritanceType.JOINED)
public class Contractor {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String fullName;
    private String shortName;
    @OneToOne
    @JoinColumn(name = "address_id")
    private Address address;
    private String vatNumber;
    private String description;
    private Integer termOfPayment;
    private BigDecimal balance;
    @OneToMany(mappedBy = "contractor", fetch = FetchType.EAGER)
    private List<ContactPerson> contactPersons = new ArrayList<>();
}

Encje główną rozszerza Customer, i o nim jest teraz mowa w tym poście:

@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "customers")
public class Customer extends Contractor {
    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
    private List<Cargo> cargos = new ArrayList<>();
    @Enumerated(EnumType.STRING)
    private PaymentRating paymentRating;
    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
    private List<WarehouseOrder> warehouseOrders = new ArrayList<>();
    @OneToMany(mappedBy = "customer", fetch = FetchType.EAGER)
    private List<CustomerInvoice> customerInvoices = new ArrayList<>();
}

DTO:

@Getter
@Setter
public class CustomerDTO {
    private Long id;
    @NotBlank(message = "Full name cannot be null")
    private String fullName;
    private String shortName;
    private Long addressId;
    @Pattern(regexp = "^[A-Z]{2}\\d{8,}$", message = "Invalid VAT number")
    private String vatNumber;
    private String description;
    @NotNull(message = "Term of payment cannot be null")
    @Min(1)
    private Integer termOfPayment;
    @NotEmpty(message = "Contact person IDs cannot be null")
    private List<@Positive(message = "Goods ID must be a positive number") Long> contactPersonIds;
}

MAPPER:

public Customer mapToEntity(CustomerDTO customerDTO){
        if (customerDTO == null){
            throw new NullEntityException("Customer data cannot be null");
        }

        Customer customer = new Customer();
        customer.setId(customerDTO.getId());
        customer.setFullName(customerDTO.getFullName());
        customer.setShortName(customerDTO.getShortName());
        Address address = addressRepository.findById(customerDTO.getAddressId()).orElseThrow(() -> new AddressNotFoundException("Address not found"));
        customer.setAddress(address);
        customer.setVatNumber(customerDTO.getVatNumber());
        customer.setDescription(customerDTO.getDescription());
        customer.setTermOfPayment(customerDTO.getTermOfPayment());

        List<ContactPerson> contact = customerDTO.getContactPersonIds().stream()
                .map(contactPersonIds -> contactPersonRepository.findById(contactPersonIds)
                        .orElseThrow(() -> new ContactPersonNotFoundException("Contact Person not found with this ID " + contactPersonIds)))
                .collect(Collectors.toList());
        customer.setContactPersons(contact);
        return customer;
    }

    public CustomerDTO mapToDTO(Customer customer){
        if (customer == null){
            throw new NullEntityException("Customer cannot be null");
        }
        CustomerDTO dto = new CustomerDTO();
        dto.setId(customer.getId());
        dto.setFullName(customer.getFullName());
        dto.setShortName(customer.getShortName());
        List<Long> contactPersons = customer.getContactPersons().stream().map(ContactPerson::getId).collect(Collectors.toList());
        dto.setContactPersonIds(contactPersons);
        dto.setVatNumber(customer.getVatNumber());
        dto.setAddressId(customer.getAddress().getId());
        dto.setDescription(customer.getDescription());
        dto.setTermOfPayment(customer.getTermOfPayment());
        return dto;
    }

SERWIS:

public List<CustomerDTO> findAllCustomersSortedBy(String sortBy) {
        return customerRepository.findAllCustomersBy(sortBy).stream().map(customerMapper::mapToDTO).collect(Collectors.toList());
    }


@Transactional
    public CustomerDTO addCustomer(CustomerDTO customerDTO) {
        Customer customer = customerMapper.mapToEntity(customerDTO);

        addAdditionalDataForCustomer(customer);
        addAdditionalDataForContactPerson(customer);

        Customer saved = customerRepository.save(customer);
        return customerMapper.mapToDTO(saved);
    }


private static void addAdditionalDataForContactPerson(Customer customer) {
        List<ContactPerson> contactPersons = customer.getContactPersons();
        if (!contactPersons.isEmpty()) {
            contactPersons.forEach(person -> person.setContractor(customer));
        }
    }

    private static void addAdditionalDataForCustomer(Customer customer) {
        customer.setBalance(BigDecimal.ZERO);
        customer.setPaymentRating(PaymentRating.NONE);
    }

REPO:

@Query("SELECT c FROM Customer c ORDER BY " +
            "CASE WHEN :sortBy = 'fullName' THEN c.fullName END ASC, " +
            "CASE WHEN :sortBy = 'shortName' THEN c.shortName END ASC, " +
            "CASE WHEN :sortBy = 'vatNumber' THEN c.vatNumber END ASC")
    List<Customer> findAllCustomersBy(@Param("sortBy") String sortBy);

Problem mam z testami do dwóch metod jakie widać w serwisie, do tej która zwraca liste klientów i ją sortuje oraz do metody która dodaje klienta. Obie te metody działają poprawnie kiedy testuje to sobie w Postmanie, wiec chyba mam jakiś problem w testach.

TESTY:

@Test
    @DisplayName("Should find all sorted Customers")
    public void testFindAllCustomersSorted_Success(){
        Customer customer1 = prepareFirstCustomer();
        Customer customer2 = prepareSecondCustomer();
        CustomerDTO customerDTO1 = prepareFirstDTO();
        CustomerDTO customerDTO2 = prepareSecondDTO();

        List<Customer> sortedCustomers = Arrays.asList(customer2, customer1);

        when(customerRepository.findAllCustomersBy("fullName")).thenReturn(sortedCustomers);
        when(customerMapper.mapToDTO(customer1)).thenReturn(customerDTO1);
        when(customerMapper.mapToDTO(customer2)).thenReturn(customerDTO2);

        List<CustomerDTO> sortedResult = customerService.findAllCustomersSortedBy("fullName");

        assertEquals(customerDTO1, sortedResult.get(0));
        verify(customerRepository, times(1)).findAllCustomersBy("fullName");

    }



    private CustomerDTO prepareFirstDTO() {
        CustomerDTO customerDTO = new CustomerDTO();
        customerDTO.setId(1L);
        customerDTO.setFullName("ABC SPED");
        return customerDTO;
    }
    private CustomerDTO prepareSecondDTO() {
        CustomerDTO customerDTO = new CustomerDTO();
        customerDTO.setId(2L);
        customerDTO.setFullName("XYZ SPED");
        return customerDTO;
    }

    private static Customer prepareSecondCustomer() {
        Customer customer = new Customer();
        customer.setId(2L);
        customer.setFullName("XYZ SPED");
        return customer;
    }

    private static Customer prepareFirstCustomer() {
        Customer customer = new Customer();
        customer.setId(1L);
        customer.setFullName("ABC SPED");
        return customer;
    }

Wg. mnie na pierwszym miejscu powinien znaleźć się ABC SPED, jednak dostaje błąd

org.opentest4j.AssertionFailedError: 
Expected :com.tsl.dtos.CustomerDTO@5dda6f9
Actual   :com.tsl.dtos.CustomerDTO@10027fc9

Oczekuje że na pierwszym miejscu bedzie XYZ, nie rozumiem dlaczego. Tak jak wspomniałem wyżej, metoda ta działa poprawnie, zwraca mi posortowaną liste.

I drugi, większy problem, test metody która dodaje klienta:


    @Test
    @DisplayName("Should add new Customer")
    public void testAddCustomer_Success(){
        CustomerDTO customerDTO = new CustomerDTO();
        customerDTO.setFullName("John Doe");
        customerDTO.setAddressId(1L);
        customerDTO.setTermOfPayment(30);
        customerDTO.setContactPersonIds(Arrays.asList(1L, 2L));

        Address address = new Address();
        address.setId(1L);

        ContactPerson contactPerson1 = new ContactPerson();
        contactPerson1.setId(1L);

        ContactPerson contactPerson2 = new ContactPerson();
        contactPerson2.setId(2L);

        when(addressRepository.findById(1L)).thenReturn(Optional.of(address));
        when(contactPersonRepository.findById(1L)).thenReturn(Optional.of(contactPerson1));
        when(contactPersonRepository.findById(2L)).thenReturn(Optional.of(contactPerson2));

        CustomerDTO result = customerService.addCustomer(customerDTO);

        verify(customerRepository, times(1)).save(any());

        assertEquals("John Doe", result.getFullName());
        assertEquals(1L, result.getAddressId());
        assertEquals(30, result.getTermOfPayment());
        assertEquals(Arrays.asList(1L, 2L), result.getContactPersonIds());
    }

Tutaj cały czas mi krzyczy ze Customer to null

java.lang.NullPointerException: Cannot invoke "com.tsl.model.contractor.Customer.setBalance(java.math.BigDecimal)" because "customer" is null

1
MaciejJankowski napisał(a):
org.opentest4j.AssertionFailedError: 
Expected :com.tsl.dtos.CustomerDTO@5dda6f9
Actual   :com.tsl.dtos.CustomerDTO@10027fc9

Tutaj warto byłoby dodać kolejną adnotację @ToString nad CustomerDTO, ewentualnie dopisać samemu, żeby chociaż pokazywało name...

Wg. mnie na pierwszym miejscu powinien znaleźć się ABC SPED

Dlaczego niby? Przecież sam mu zadeklarowałeś, że

List<Customer> sortedCustomers = Arrays.asList(customer2, customer1);
when(customerRepository.findAllCustomersBy("fullName")).thenReturn(sortedCustomers);

więc zwraca dokładnie w takiej kolejności, w jakiej podałeś. Dobitnie to tylko pokazuje, że nie testujesz tu żadnej logiki.

Tutaj cały czas mi krzyczy ze Customer to null

java.lang.NullPointerException: Cannot invoke "com.tsl.model.contractor.Customer.setBalance(java.math.BigDecimal)" because "customer" is null

Bo nie dałeś Mockito.when dla mappera.

W ogóle, tyle warte te testy że ja bym je po prostu usunął xd

0

To jak powinny wyglądać testy które są dobre ? Dopiero zaczynam, nie mam tyle wiedzy co Ty kolego :)

0

Faktycznie zapomniałem o maperze, jednak teraz krzyczy że result jest nullem.

1

@MaciejJankowski Cześć! Witaj na forum, fajnie że zadałeś pytanie.

Od razu plusik dla Ciebie że od razu wkleiłeś cały kod — mało kto to dodaje, a to bardzo pomaga udzielić pomocy 😉.

Co do samego kodu

MaciejJankowski napisał(a):

    @Test
    @DisplayName("Should add new Customer")
    public void testAddCustomer_Success(){
        CustomerDTO customerDTO = new CustomerDTO();
        customerDTO.setFullName("John Doe");
        customerDTO.setAddressId(1L);
        customerDTO.setTermOfPayment(30);
        customerDTO.setContactPersonIds(Arrays.asList(1L, 2L));

        Address address = new Address();
        address.setId(1L);

        ContactPerson contactPerson1 = new ContactPerson();
        contactPerson1.setId(1L);

        ContactPerson contactPerson2 = new ContactPerson();
        contactPerson2.setId(2L);

        when(addressRepository.findById(1L)).thenReturn(Optional.of(address));
        when(contactPersonRepository.findById(1L)).thenReturn(Optional.of(contactPerson1));
        when(contactPersonRepository.findById(2L)).thenReturn(Optional.of(contactPerson2));

        CustomerDTO result = customerService.addCustomer(customerDTO);

        verify(customerRepository, times(1)).save(any());

        assertEquals("John Doe", result.getFullName());
        assertEquals(1L, result.getAddressId());
        assertEquals(30, result.getTermOfPayment());
        assertEquals(Arrays.asList(1L, 2L), result.getContactPersonIds());
    }

Ten test jest bardzo słaby. Rozumiem, że taki napisałeś, bo gdzieś w jakimś tutorialu znalazłeś przykład jak korzystać z jUnita i pisać testy, ale testy napisane w taki sposób bardzo Ci nie pomogą.

Taki test:

  • Jest zbyt skomplikowany - zbyt dużo czasu i energii na pewno poświęciłeś żeby go napisać
  • Jest za długi - ciężko zrozumieć co on testuje, ciężko się go czyta
  • Ma zbyt dużo szczegółów - czyli pewnie dowolna zmiana w kodzie zepsuje ten test
  • Polega na szczegółach implementacyjnych zamiast na interfejsach.

Pamiętaj ze test ma tylko jedno zadanie: Testy ma Ci udzielić jednoznacznej odpowiedzi, czy jakaś mała funkcjonalność nadaje się do wdrożenia. Zauważ że powiedziałem "mała funkcjonalność", a nie "mały kawałek kodu".

Widzę, że Ty chcesz dodać test, który sprawdza czy dodano nowego klienta. Taki test powinien wyglądać więc tak:

@Test
public void shouldAddCustomer() {
  // when
  addCustomer("John Doe", 1L, 30, emptyList());
  // then
  assertCustomerExists("John Doe");
}

Zauważ że zawrałem tu mało szczegółów, nie wspomniałem w ogóle o service ani repo, i przekazałem pustą listę do personsId - bo gdybym chciał to przetestować, to od tego powinien być osobny test - tylko pod personsIds.

Natomiast co do kodu też można się przyczepić:

  • Nazywasz klasę DTO, ale masz w niej logikę walidującą. Ergo, ta klasa nie może być DTO (bo DTO z definicji to pusta struktura która nie ma w sobie nic oprócz pól).
  • Nazywasz klasę "Service", ale tam praktycznie nie ma żadnej logiki (cała logika jest porozrzucana po innych klasach).
  • Tylko repo się dobrze nazywa, bo faktycznie jest repo. Aczkolwiek porządne repo powinno zwrócić pustą klasę (Plain-Old Java Object, albo obiekt domenowy). U Ciebie zwraca obiekt ORM'owy, czyli to repo tak na prawdę nie spełnia swojej roli odpowiednio. Tzn. to jest repozytorium JPA-owe, jasne. Ale nie jest takim repozytorium "programistycznym", czyli niedostatecznie dobrze chowa persystencje w sobie.
0

Bardzo Ci dziękuje za pomocną odpowiedź, jak widać można odpisać z kulturą :)
Pozostaje mi poszukać jakiegoś dobrego kursu do testów, gdyż przyznam że nie robiłem tego wcześniej.

2
MaciejJankowski napisał(a):

Bardzo Ci dziękuje za pomocną odpowiedź, jak widać można odpisać z kulturą :)
Pozostaje mi poszukać jakiegoś dobrego kursu do testów, gdyż przyznam że nie robiłem tego wcześniej.

Zacznij od tych (są po angielsku):

Posortowałem je w rosnąco po poziomie trudności.

0

Natomiast co do kodu też można się przyczepić:

  • Nazywasz klasę DTO, ale masz w niej logikę walidującą. Ergo, ta klasa nie może być DTO (bo DTO z definicji to pusta struktura która nie ma w sobie nic oprócz pól).
  • Nazywasz klasę "Service", ale tam praktycznie nie ma żadnej logiki (cała logika jest porozrzucana po innych klasach).
  • Tylko repo się dobrze nazywa, bo faktycznie jest repo. Aczkolwiek porządne repo powinno zwrócić pustą klasę (Plain-Old Java Object, albo obiekt domenowy). U Ciebie zwraca obiekt ORM'owy, czyli to repo tak na prawdę nie spełnia swojej roli odpowiednio. Tzn. to jest repozytorium JPA-owe, jasne. Ale nie jest takim repozytorium "programistycznym", czyli niedostatecznie dobrze chowa persystencje w sobie.

Uważam że problem jest przede wszystkim taki że co programista to inna szkoła i opinia, robie jeden kurs, ktoś uczy tak, robie drugi kurs, ktoś uczy inaczej, pisze na forum post, ktoś uważa tak, ktoś inny inaczej. Oczywiście pewnie są jakieś złote zasady dobre do wszystkiego ale odnosze wrażenie że o cokolwiek bym nie zapytał na forum zawsze dostaje różne informacje.

Kiedyś miałem problem z mapperem, to ktoś mi napisał po co w ogóle robie tego mappera, powinienem użyć MapStruct.
Wykupiłem kiedyś lekcje z mentorem, dostałem info że generalnie lepiej robić tak jak robie.
Z DTO podobnie, dziś czytam że błędem jest robienie walidacji na DTO, a kilka wątków temu ktoś się dziwił czemu NIE robie walidacji na DTO skoro przyjmuje go jako argument.

Przyznasz że można się troche zakręcić, zwłaszcza jak jest się na początku drogi.

I potem piszesz taki post aby ktoś Ci coś podpowiedział bo od miesięcy uczysz się sam i masz wrażenie że stoisz w miejscu, i jeden kolega z drugim smieszkiem komentują że generalnie to wywal te testy, i młode pokolenie jest wrażliwe, gdzie tak btw to do młodego pokolenia troche mi brakuje :)

Temat do zamknięcia. Na tym forum jest garstka ludzi którzy potrafią normalnie odpisać i nakierować, reszta to bardzo smutni i sfrustrowani ludzie :)

0
MaciejJankowski napisał(a):

Uważam że problem jest przede wszystkim taki że co programista to inna szkoła i opinia, robie jeden kurs, ktoś uczy tak, robie drugi kurs, ktoś uczy inaczej, pisze na forum post, ktoś uważa tak, ktoś inny inaczej. Oczywiście pewnie są jakieś złote zasady dobre do wszystkiego ale odnosze wrażenie że o cokolwiek bym nie zapytał na forum zawsze dostaje różne informacje.

Kiedyś miałem problem z mapperem, to ktoś mi napisał po co w ogóle robie tego mappera, powinienem użyć MapStruct.
Wykupiłem kiedyś lekcje z mentorem, dostałem info że generalnie lepiej robić tak jak robie.

MaciejJankowski napisał(a):

Przyznasz że można się troche zakręcić, zwłaszcza jak jest się na początku drogi.

No fakt, to może być bardzo frustrujące. Doskonale Cię rozumiem.

MaciejJankowski napisał(a):

Z DTO podobnie, dziś czytam że błędem jest robienie walidacji na DTO, a kilka wątków temu ktoś się dziwił czemu NIE robie walidacji na DTO skoro przyjmuje go jako argument.

Mógłbyś podlinkować? Zobaczymy co my tam mamy.

Ogólnie walidacja powinna być, ale w odpowiednim miejscu. I nazwy powinny coś znaczyć. Nazwa "DTO" sugeruje że celem istnienia takiej klasy, jest po prostu przenieść dane z jednego miejsca w drugi. I takie coś nie powinno mieć żadnej logiki w sobie - a jeśli ma, to z definicji nie jest DTO'sem; tylko wtedy jest np modelem biznesowym. Więc jeśli chcesz żeby w Twojej klasie CustomerDTO nadal była logika walidacji, to to nie jest DTO i powinieneś zmienić nazwę tej klasy, np na Customer - tak żeby traktować go jak model biznesowy. Natomiast jeśli głównym celem klasy CustomerDTO jest to że ma przenosić dane z jednego miejsca w inne, to walidacja tych danych powinna być w miejscu gdzie się przyjmuje ten obiekt (czyli w jakimś entry poincie, możliwe że controlerze).

MaciejJankowski napisał(a):

I potem piszesz taki post aby ktoś Ci coś podpowiedział bo od miesięcy uczysz się sam i masz wrażenie że stoisz w miejscu, i jeden kolega z drugim smieszkiem komentują że generalnie to wywal te testy, i młode pokolenie jest wrażliwe, gdzie tak btw to do młodego pokolenia troche mi brakuje :)

Czasami wywalenie jakiegoś kodu to jest krok w przód.

0

Masz w repo jakąś metodę z pięknym sql'em a tymczasem ja bym oczekiwał, że skoro piszesz unit testy to w klasie testowej będziesz miał jakąś implementację w pamięci (np., w postaci hashmapy) któa będzie szybka ale która może nie mieć nic wspólnego z zapytaniem nad metodą w repo którego kawałek wkleiłeś.

Za mało trochę imho pokazałeś.

0

Przy tworzeniu nowego użytkownika a w dto chyba powinno być do adresu

    private String zipCode;
    private String street;
    private String houseNumber;

i w maperze

Address address = new Address();
address.setCity(dto.getCity());
        address.setZipCode(dto.getZipCode());
        address.setStreet(dto.getStreet());
        address.setHouseNumber(dto.getHouseNumber());

        person.setAddress(address);

Bo teraz szukasz adresu po id chyba że wcześniej już go zapisałeś.

1 użytkowników online, w tym zalogowanych: 0, gości: 1