Inicjalizacja MockMvc w metodzie setUp kompletnie nie działa.

0

Cześć,
Chciałem napisać swój pierwszy test endpointu jednak po 2 dniach zmaganiach postanowiłem się poddać i napisać na forum :P
Do rzeczy.
Dostaję błąd jaki jest w załączniku. Z tego co próbowałem na różne sposoby go rozwiązać to nie odpala mi się metoda z adnotacją @BeforeAll albo występują inne dziwne błędy z obiektem MockMvc.
Chciałem koniecznie w taki sposób napisać ten test ( metoda która mi zamieni DTO na JSON i odwrotnie, principal ponieważ mam go w metodzie w kontrolerze, MockHttpServletResponse aby sprawdzić sobie przy okazji np. status) Różne inne próby innych konwersji json to java object kończyły się NPE z obiektu DTO więc podejrzewam, że ja w ogóle mogę źle te adnotację to testów rozumieć z tego wszystkiego...
Czekam na uwagi również pod kątem czy w taki sposób się to powinno robić ponieważ szukając informacji o testach jednostkowych znalazłem ogrom przeróżnych możliwości testowania więc wybrałem pierwszą lepszą z brzegu która wydawała mi się najbardziej ciekawa co do testowania akurat endpointów.

Klasa testowa:


import java.io.IOException;
import java.security.Principal;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@ExtendWith(MockitoExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ToDoListControllerTest {

	@Mock
	private Principal principal;

	private MockMvc mockMvc;

	@Autowired
	private ObjectMapper objectMapper;

	@InjectMocks
	private ToDoListController toDoListController;

	@Mock
	private ToDoListService toDoListService;

	@Mock
	private UserService userService;

	@BeforeAll
	void setup() {
		mockMvc = MockMvcBuilders.standaloneSetup(toDoListController).build();
	}

	private String mapToJson(Object obj) throws JsonProcessingException {
		ObjectMapper objectMapper = new ObjectMapper();
		return objectMapper.writeValueAsString(obj);
	}

	private <T> T mapFromJson(String json, Class<T> clazz)
			throws JsonParseException, JsonMappingException, IOException {

		ObjectMapper objectMapper = new ObjectMapper();
		return objectMapper.readValue(json, clazz);
	}

	@Test
	public void toDoListCannotBeSavedWhenNameLengthIsInvalid() throws Exception {

		// given
		ToDoListDTO toDoListDTO = new ToDoListDTO();
		toDoListDTO.setName("xd");
		String inputJson = mapToJson(toDoListDTO);
		given(principal.getName()).willReturn("user123");

		// when
		MockHttpServletResponse response = mockMvc.perform(post("/api/ToDoList/").principal(principal)
				.contentType(MediaType.APPLICATION_JSON).content(inputJson).accept(MediaType.APPLICATION_JSON))
				.andReturn().getResponse();
		// Then
		assertThat(response.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
	}
}

Klasa kontrolera:

@RestController
public class ToDoListController {

	private ToDoListService toDoListService;
	private UserService userService;

	@Autowired
	public ToDoListController(ToDoListService toDoListService, UserService userService) {
		this.toDoListService = toDoListService;
		this.userService = userService;
	}

	@RequestMapping(value = "/api/ToDoList", method = RequestMethod.POST)
	public ResponseEntity<ToDoList> addNewList(Principal principal, @Valid @RequestBody ToDoListDTO todoListDTO,
			BindingResult result) {

		String username = principal.getName();

		if (result.hasErrors()) {
			List<FieldError> errors = result.getFieldErrors();
			for (FieldError error : errors) {
				System.out.println(error.getObjectName() + " " + error.getDefaultMessage());

			}
			return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
		}

		User user = userService.getUserRepository().findByUsername(username);

		ToDoList toDoList = new ToDoList();
		toDoList.setName(todoListDTO.getName());
		user.addToDoList(toDoList);
		userService.getUserRepository().save(user);

		return ResponseEntity.status(HttpStatus.CREATED).build();
	}

Klasa DTO

public class ToDoListDTO {

	@NotEmpty 
	@Length(min = 3, max = 50)
	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

}
0

Coś się załącznik nie dodał

java.lang.NullPointerException
	at org.springframework.test.web.servlet.setup.StubWebApplicationContext.addBeans(StubWebApplicationContext.java:148)
	at org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder.registerMvcSingletons(StandaloneMockMvcBuilder.java:374)
	at org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder.initWebAppContext(StandaloneMockMvcBuilder.java:364)
	at org.springframework.test.web.servlet.setup.AbstractMockMvcBuilder.build(AbstractMockMvcBuilder.java:132)
	at pl.XXXz.list.ToDoListControllerTest.setup(ToDoListControllerTest.java:52)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:532)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.lambda$invokeBeforeAllMethods$6(ClassTestDescriptor.java:239)
	at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.invokeBeforeAllMethods(ClassTestDescriptor.java:238)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.before(ClassTestDescriptor.java:164)
	at org.junit.jupiter.engine.descriptor.ClassTestDescriptor.before(ClassTestDescriptor.java:65)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:105)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1540)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$4(NodeTestTask.java:112)
	at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:72)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:98)
	at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:74)
	at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
	at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
	at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:137)
	at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:89)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)

2
  1. Serio takie coś :
        User user = userService.getUserRepository().findByUsername(username);

        ToDoList toDoList = new ToDoList();
        toDoList.setName(todoListDTO.getName());
        user.addToDoList(toDoList);
        userService.getUserRepository().save(user);

W kontrolerze? o_O

  1. Ja bym sugerował w ogóle wywalić ten cały mockmvc i zamiast polegać na jakiejs magii i cudach, to przetestować to po ludzku, stawiając w teście aplikacje i wysyłając do niej prawdziwy request http i patrząc co aplikacja zwróciła.
0

Mock mocka mockiem pogania : / Juz raczej bym odpalił context i strzelał "normalne" requesty w kontrolery niż mockował cały świat.
A najlepiej to wyjąłbym całą te logike z kontrolera do osobnej klasy i przetestował to unitowo. Bo chyba te logike właśnie chcesz przetestować :)

4

A najlepiej to wyjąłbym całą te logike z kontrolera do osobnej klasy i przetestował to unitowo. Bo chyba te logike właśnie chcesz przetestować

Można tylko czy warto? Skoro wystarczy mały krok w przód i testujesz realnie jak zachowuje się aplikacja w "całości" razem np. z autentykacją/autoryzacja, mapowaniem obiektów z/na jsona itd.
Unity są ok jak chcesz testować jakiś szczególny wycinek kodu w oderwaniu od innych rzeczy. No i to zawsze testy "wewnętrznych" bebechów systemu, podczas gdy w praktyce najbardziej interesują cię zwykle "testy akceptacyjne" / testy funkcjonalności. Chcesz wiedzieć czy aplikacja jako taka zachowuje się zgodnie z oczekiwaniami.
W czasach gigantycznych monolitów i braku lekkich baz danych in memory, lekkich kontenerów webowych itd. pisanie masy unit testów może i miało sens, bo trudno było w teście postawić sobie bazę danych czy jakiś inny "ciężki" komponent, ale czasy się trochę zmieniły. Masz mikroserwisy, które wcale nie maja przytłaczajacej liczby use-casów, masz lekkie rzeczy które można wystartować w mgnieniu oka, masz dockeryzowane nawet jakieś dziwne rzeczy (patrz: testcontainers).

Zresztą co tu niby kolega przetestuje jak on wszystko w tym kodzie zmockował :D Usuniesz cały kod z tego projektu a ten test nadal będzie zielony.

0
Shalom napisał(a):
  1. Serio takie coś :
        User user = userService.getUserRepository().findByUsername(username);

        ToDoList toDoList = new ToDoList();
        toDoList.setName(todoListDTO.getName());
        user.addToDoList(toDoList);
        userService.getUserRepository().save(user);

W kontrolerze? o_O

Czemu tutaj to jest złe? :)

2

Bo kontroler to nie jest miejsce na logikę aplikacji tylko na związanie infrastruktury (w tym przypadku RESTowego endpointu) z logiką. Wyobraź sobie że chcesz teraz tą logikę serwować innym interfejsem, nie RESTem, jak to zrobisz skoro zaszyłeś logikę w kontrolerze? :)

0

Troszkę zmieniając temat, ale nadal o testowaniu: co jeśli mam CF(CompletableFuture) - endpoint go wywołuje, jak takie coś otestować? Np. sprawdzić czy pobrał on odpowiednią ilość danych?

0

Pokaż przykład o co ci chodzi bo nie bardzo rozumiem. Zwracasz CompletableFuture z kontrolera? W takim przypadku Spring robi trochę magii i w praktyce zwraca zwyczajnie wynik, jak tylko się pojawi.
Chyba że pytasz o testowanie asynchronicznych operacji, tzn endpoint triggeruje jakieś asynchroniczne zachowanie które kiedyś sie wykona. Możesz do tego użyć jakiegoś Awaitility (zwykły wrapper na pętlę ze sleepem która periodycznie sprawdza jakis warunek i failuje z timeoutem) -> czekasz na wystąpienie jakiejś sytuacji zadaną ilość czasu.

0

Ja też tak wracając do testów.
Czy można to testować tworząc po prostu obiekty Controller i z tego robić asercje testów czy będzie to za ciężkie?

1

Zadaj sobie podstawowe pytanie, co chcesz osiągnąć. Skoro w kontrolerze nie ma logiki, to po co chcesz go tworzyć ręcznie i wołać?

0

Chłopaki, bo ja już się chyba w tym wszystkim pogubiłem xd
Testować mam metody w serwisie np.

    User user = userService.getUserRepository().findByUsername(username);

    ToDoList toDoList = new ToDoList();
    toDoList.setName(todoListDTO.getName());
    user.addToDoList(toDoList);
    userService.getUserRepository().save(user);

Czyli mam sobie napisać test który sprawdzi co się stanie jak będę chciał dodać liste która ma mniej niż 3 znaki - rzuci wyjątek albo tego obiektu nie doda mi do bazy?

Zrozumiałem chociaż tyle, że MockMvc jest złi nie powinienem ( nie tworzyć testów klas Controller ) go używać w ogóle tylko RĘCZNIE testować enpointy w postmanie? No bo jak niby napisać kontroler, z niego wyciągnąć metode addNewList skoro w sygnaturze mam jeszcze zestaw atrybutów (Principal principal, @Valid @RequestBody ToDoListDTO todoListDTO, BindingResult result) które trzeba byłoby osobno znów mockować/testować?

0

MockMvc nie jest zły. Można go użyć jak takiego postmana ale w testach xd Stawiasz w tescie kontekst springa, czyli prawie jakbyś odpalił aplikacje i potem przykładowo tym mockmvc możesz w nią strzelać i testować responsy. W Twoim kodzie nie używałeś tylko mockmvc tylko zamockowałeś wszystko co istnieje. Albo stawiasz kontekst i walisz w kontrolery/endpointy albo nie stawiasz kontekstu i testujesz sobie jakies serwisy, ktore nic nie wiedza o zadnych restach

2

@Sasori: no testujesz to co chcesz przetestować o_O Nie ma jakiejś "zasady". Jeśli chcesz sprawdzić co się stanie jak będę chciał dodać liste która ma mniej niż 3 znak to piszesz taki właśnie test. Ale testowanie repozytorium to akurat miejsce na testy integracyjne z jakąś bazą in memory, a nie na unit testy z mockami ;)

tylko RĘCZNIE testować enpointy w postmanie?

Ale czemu? o_O Tzn no możesz ale to głupie. Czemu nie napiszesz testu który startuje aplikacje (SpringBootTest to ogarnia) tworzy sobie obiekt HttpClient i wysyła request do localhost:jakiśtamPort i potem sprawdza co dostał od aplikacji?

Problem z MockMVC jest taki że on nie przetestuje ci wszystkiego bo "wpina się" pod kontroler dopiero. Dlatego lepiej stukać klientem http do stojącej apki zamiast tego, bo koszt niewielki a testujesz więcej.

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