JUnit ile Servis Katmanı Testi-1
Bir önceki Controller testi yazısını okumadıysanız onunla başlayabilirsiniz.
Bu yazımızda öncelikle servis katmanı oluşturacağız bu bu katman için testler yazmaya çalışacağız.
UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
@Service @Slf4j public class UserService { @Autowired private UserRepository userRepository; @Autowired private AmqpTemplate rabbitTemplate; @Autowired private PaymentClient paymentClient; public User createUser(User user) { Address address = new Address("Türkiye", "İstanbul", "Açık address"); user.setAddress(address); List<Customer> customers = new ArrayList<>(); customers.add(Customer.builder().name("Cem").build()); customers.add(Customer.builder().name("Elif").build()); customers.add(Customer.builder().name("Emir Asaf").build()); customers.add(Customer.builder().name("Nehir").build()); Payment payment = paymentClient .createPayment(new Payment(LocalDateTime.now(), CurrencyType.TL, BigDecimal.TEN)); if (payment.getAmount().intValue() > 1000) { user.setFirmType(FirmType.CORPORATE); rabbitTemplate.convertAndSend("notification.email", } else { user.setFirmType(FirmType.INDIVIUAL); } log.info(payment.toString()); user.setCustomerList(customers); return userRepository.save(user); } } |
Yukarıdaki gibi bir örnek sınıf oluşturdum, method içerisindeki kodlar bir anlam ifade etmemektedir.
Tamamen karşılaşabileceğiniz senaryolara benzerlik katabilmek için oluşturduğum kodlardır.
- Şimdi bu method neleri içeri biraz inceleyelim.
- Address nesnesi oluşturuyor.
- Customer listesi oluşturuyor.
- PaymentClient kullanarak payment nesnesi oluşturuyor.
- Payment nesnesi üzerinden bir kontrol yapıyor ve dallanma oluşuyor.
- En son olarak ise method görevi gereği UserRepository kullanarak kayıt yapıyor.
Genellikle ilk başlayanlar içi kafa karıştırıcı durum methodun yapmasını beklediğimiz işler ile bağımlı olduğu işleri karıştırım nasıl bir test yazacağını bilemem oluyor.
Örneğin zaten methodun kendi görevi olan Address nesnesini de test içerisinde oluşturmaya çalışıyor(Kendi gözlemim).
Biz ise bağımlılıkları anlayıp methoddan üzerine düşen görevi farklı durumlarda yani farklı verilerde istediğimiz işi yapıyor mu diye doğrulamaya çalışıyoruz.
Peki neler olabilir bu beklediğimiz durumlar?
- Belli koşullarda hata verebilir.
- Koşullara bağlı olarak bazı işler yapılırken yani methodlar çağrılırkenbazı durumlarda çağrılmaması olabilir.
- Kodun gerekliliği olarak bazı method çağrılarını belli sayıda gerçekleştirilmesi olabilir. Örneğin bir method yalnızca iki kere çağrılması gerekebilir ve eğer üçüncü kez çağrılırsa bir sorun var demektir.
- Bazı değişkenlerin istediğimiz değerlere sahip olması olabilir.
Bu bakış açısıyla testimizi yazmaya çalışalım.
Talk cheap, show me the code.
Unit Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.hamcrest.CoreMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class UserServiceTest { @InjectMocks private UserService userService; @Mock private RabbitTemplate rabbitTemplate; @Mock private PaymentClient paymentClient; @Mock private UserRepository userRepository; @Test @DisplayName("it should create corporate user when payment amount is greater than 1000") void it_should_create_corporate_user_when_payment_amount_greater_then_1000() { // given Payment payment = new Payment(LocalDateTime.now(), CurrencyType.TL, new BigDecimal(1001)); //createPayment methodu çağrıldığında oluşturduğumuz payment objesi dönmeli Mockito.when(paymentClient.createPayment(Mockito.any())).thenReturn(payment); //mock User user = prepareUser(); Mockito.when(userRepository.save(Mockito.any())).thenReturn(user);//mock // when //test etmek istediğimiz methodu çağırıyoruz User response = userService.createUser(user); // then // bir kere çağrıldığından emin olmak için ekliyoruz verify(userRepository, times(1)).save(Mockito.any()); //bir kere ve bizim istediğimiz değerler ile çalıştığından emin olmak için ekliyoruz verify(rabbitTemplate, times(1)).convertAndSend(Mockito.eq("notification.email"), Mockito.any(EmailDto.class)); //bizim beklediğimiz değerler oluşuyor mu diye kontrol ediyoruz. assertThat(response.getName()).isEqualTo(user.getName()); assertThat(response.getEmail()).isEqualTo(user.getEmail()); assertThat(response.getFirmType()).isEqualTo(FirmType.CORPORATE); } private User prepareUser() { User user = new User(); user.setName("Cem"); return user; } } |
Servis methodu içerisindeki kırılımdan bahsetmiştik. Bir kırılımla beraber methotdan beklentimiz değişiyor.
Dolayısıyla farklı değerlerle methotdan ve dolayısıyla testten beklentimiz değişiyor.(bknz: branch testing)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
Test @DisplayName("it should create individual user when payment amount is less than 1000") void it_should_create_indivual_user_when_payment_amount_less_then_1000() { // When Payment payment = new Payment(LocalDateTime.now(), CurrencyType.TL, new BigDecimal(100)); Mockito.when(paymentClient.createPayment(Mockito.any())).thenReturn(payment); User user = prepareUser(); Mockito.when(userRepository.save(Mockito.any())).thenReturn(user); // given User response = userService.createUser(user); // then //bir kere herhangi bir obje ile UserRepository çağrıldığını kontrol ediyoruz verify(userRepository, times(1)).save(Mockito.any()); //rabbitTemplate diğer senaryoda kullanıldığı için burada method çağrılmadığından emin olmamız gerekir. verifyNoInteractions(rabbitTemplate); assertThat(response.getName()).isEqualTo(user.getName()); assertThat(response.getEmail()).isEqualTo(user.getEmail()); assertThat(response.getFirmType()).isEqualTo(FirmType.INDIVIUAL); } |
Not: Zaman zaman gördüğüm bir davranış var bunu da paylaşmak istiyorum sizinle. O da şu ki yeni bir özellik eklendiğinde var olan bir test methodu hata verdiğinde bunu düzenlemek yerine silmek üzerine bir davranış oluşuyor ve ya disable etmeyi çözüm olarak düşünüyoruz fakat bu tabii ki doğru olmayan bir pratik. Çünkü hali hazırda yazılmış belirli kurallara göre çalışan bir test mevcut ve bu testin hata vermesi çok doğal. Bunu sildiğimiz durumda muhtemelen yeni bir özellik eklemek yerine sistemde yeni bug’lar oluşturuyoruz. Biz oluşturmasak bile bizden sonraki kişilerin oluşturması muhtemel. Bu sebeple sorunları görmezden gelmek yerine çözmeye çalışmak yeni sorunların oluşmasını engelleyecektir.
Test yazalım, yazmayanları uyaralım 😇😊
Tabii ki daha iyi bir test yazılabilir, farklılıklar olabilir. Yeni yazılarda bazı kısımları değiştiriyor olacağız. Yeni yazılarda görüşmek üzere.
Faydalı olması dileğiyle.