Mock & MockBean & Spy & SpyBean

Mock & MockBean & Spy & SpyBean

Ten artykuł na celu wyjaśnić różnicę między 4 typami adnotacji @Mock & @MockBean & @Spy & @SpyBean. Na początek zacznijmy od pobrania projektu – https://github.com/mwarycha/JavaLeader.pl/tree/master/bean-rest-validation – jest to projekt w którym zdefiniowano obiekt domenowy – Employee wraz z podstawowymi walidacjami. Po więcej informacji zapraszam do artykułu – https://javaleader.pl/2021/10/13/rest-api-i-walidacja-beana/. Napiszmy testy integracyjne:

@WebMvcTest(EmployeeController.class)
@Import(PrinterLog.class)
public class EmployeeTest {
 
  @Autowired
  private MockMvc mockMvc;
 
  @Mock
  PrinterLog printerLog;
 
  @Test
  void validEmployeeSuccessObject() throws Exception {
 
    Mockito.when(printerLog.printSuccess("employee is valid")).thenReturn("[OK] employee is valid");
 
    MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/emp")
            .contentType(MediaType.APPLICATION_JSON)
            .content(jsonSampleSuccessData());
 
    this.mockMvc.perform(builder)
            .andDo(result -> System.out.println(result.getResponse().getContentAsString()))
            .andExpect(status().isOk());
 
  }
 
  private String jsonSampleSuccessData() {
    return "{\"name\":\"Java\",\"surname\":\"Leader\",\"salary\":1000,\"role\":\"DEV\"}";
  }
 
}

gdzie klasa PrinterLog:

@Component
public class PrinterLog {
 
    public String printError(String error) {
        return "[ERROR] " + error;
    }
 
    public String printSuccess(String success) {
        return "[SUCCESS] " + success;
    }
}

oraz RestController gdzie zdefiniowany jest endpoint który testujemy:

@RestController
public class EmployeeController {
 
    @Autowired
    PrinterLog printerLog;
 
    @PostMapping("/emp")
    public ResponseEntity<String> addEmployee(@Valid @RequestBody Employee employee) {
        return ResponseEntity.ok(printerLog.printSuccess("employee is valid"));
    }
}

Zastosowanie adnotacji Mock spowoduje, że obiekt PrinterLog będzie całkowicie atrapą obiektu co oznacza, że nie będzie posiadał żadnych metod z rzeczywistą implementacją. Bardzo ważne jest zastosowanie w powyższym przykładzie adnotacji @Import(PrinterLog.class). Jeśli jej nie użyjemy nie będziemy wstanie wstrzyknąć do testowanego kontrolera implementacji klasy PrinterLog co w konsekwencji zakończy się błędem:

Error creating bean with name 'employeeController': Unsatisfied dependency expressed through field 'printerLog';

wynika to z faktu, że klasa PrinterLog jest poza kontekstem Springa i musimy ją jawnie tutaj wskazać/zaimportować. Dodajmy linie:

System.out.println(printerLog.printSuccess("success"));
System.out.println(printerLog.printError("error"));

do metody testowej w następujący sposób:

@Test
void validEmployeeSuccessObject() throws Exception {
 
  Mockito.when(printerLog.printSuccess("employee is valid")).thenReturn("[OK] employee is valid");
 
  System.out.println(printerLog.printSuccess("success"));
  System.out.println(printerLog.printError("error"));
 
  MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.post("/emp")
          .contentType(MediaType.APPLICATION_JSON)
          .content(jsonSampleSuccessData());
 
  this.mockMvc.perform(builder)
          .andDo(result -> System.out.println(result.getResponse().getContentAsString()))
          .andExpect(status().isOk());
 
}

w wyniku otrzymamy:

null
null
[SUCCESS] employee is valid

można zadać sobie teraz pytanie – dlaczego w RestController użyta została rzeczywista implementacja a nie ta której zachowanie zdefiniowane zostało w linii niżej?

Mockito.when(printerLog.printSuccess("employee is valid")).thenReturn("[OK] employee is valid");

Wynika to z faktu, że spring używa rzeczywistej implementacji zaimportowanej klasy w swoim kontekście:

@Import(PrinterLog.class)
  • Adnotacja @Mock nie powoduje, że spring będzie zarządzał obiektami klasy ją oznaczonych.

Zmieńmy to zachowanie i zastosujmy teraz adnotację @MockBean:

@MockBean
PrinterLog printerLog;

wynik:

null
null
[OK] employee is valid

Od teraz spring używa w swoim kontekście atrapy obiektu gdzie zdefiniowaliśmy zachowanie:

Mockito.when(printerLog.printSuccess("employee is valid")).thenReturn("[OK] employee is valid");

nie jest potrzebna adnotacja:

@Import(PrinterLog.class)
  • ponieważ adnotacja @MockBean spowoduje, że spring będzie zarządzał obiektami klasy ją oznaczonych.

Zarówno adnotacja @Mock jak i @MockBean to adnotacje które tworzą atrapy obiektów. Różnica polega na tym, że @MockBean tworzy beana zarządzanego przez springa.

Zmieńmy to zachowanie i zastosujmy teraz adnotację @Spy – konieczne jest użycie adnotacji @Import(PrinterLog.class) z tego samego powodu co powód opisany dla adnotacji @Mock wyżej.

@Spy
PrinterLog printerLog;

wynik:

[SUCCESS] success
[ERROR] error
[SUCCESS] employee is valid

spring używa rzeczywistej implementacji zaimportowanej klasy w swoim kontekście:

@Import(PrinterLog.class)

Obiekty typu Spy to obiekty które nie są atrapami ale możemy modyfikować ich zachowanie.

  • Adnotacja @Spy nie powoduje, że spring będzie zarządzał obiektami klasy ją oznaczonych.

Zmieńmy to zachowanie i zastosujmy teraz adnotację @SpyBean – nie jest wymagana adnotacja:

@Import(PrinterLog.class)
  • ponieważ adnotacja @SpyBean spowoduje, że spring będzie zarządzał obiektami klasy ją oznaczonych.

wynik:

[OK] employee is valid
[ERROR] error
[OK] employee is valid

Od teraz spring używa w swoim kontekście rzeczywistego obiektu gdzie zmodyfikowaliśmy zachowanie:

Mockito.when(printerLog.printSuccess("employee is valid")).thenReturn("[OK] employee is valid");

Zarówno adnotacja @Spy jak i @SpyBean to adnotacje które tworzą rzeczywiste obiekty kórych zachowanie możemy nadpisać. Różnica polega na tym, że @SpyBean tworzy beana zarządzanego przez springa.

1 Comment

  1. Władimir Włóżdo 11 stycznia 2023 at 22:31

    Super artykuł i wszystko jasne!

Leave a comment

Your email address will not be published.


*