Pavel Müller
24.10.2013

Testování Spring MVC controlleruTestování Spring MVC controllerů

Až dosud byly v zásadě dvě možnosti, jak automatizovaně testovat webovou vrstvu aplikace postavené na Springu: Selenium testy nebo klasické unit testy. Obojí má svoje nevýhody. Od verze Spring Frameworku 3.2 lze testovat Spring MVC controllery novým způsobem a myslím, že stojí za to.

Selenium testy, které v AspectWorks bohatě využíváme, mají dvě nevýhody:

  • často je píší testeři a ne vývojáři
  • je poměrně obtížné dosáhnout vysokého pokrytí kódu testy

Druhou možností je napsat klasický unit test i třeba s využitím springovských mock objektů. Sice se tím dosáhne skvělého pokrytí, ale problém je, že to většinu funkčnosti vlastně vůbec netestuje, protože je skryta kdesi v anotacích a konfiguraci.

Teď je tu ale nová možnost, bez zmíněných nevýhod. Projekt existoval již dřívě na Githubu, ale jako součást Spring Frameworku byl začleněn až od verze 3.2. Jak to funguje?

HTTP požadavek je zpracováván tak, jak by k tomu docházelo ve webovém kontejneru. Kdesi na pozadí se tak během unit testu rozběhne DispatcherServlet a požadavek se pak zpracovává standardní cestou. Ale samozřejmě bez jakéhokoliv serveru nebo kontejneru, pouze se Spring kontextem. Větší podrobnosti nechávám na dokumentaci, lepší bude nějaký příklad.

Mějme například takový jednoduchý controller pro editaci uživatele. Příklad pochází z firemního školení, takže je velmi jednoduchý, ale i tak v něm je několik záludností. Metoda prepareUser() se volá před požadavkem GET i POST a stejně tak metoda initBinder(). Nový způsob unit testů otestuje i takové návaznosti.

@Controller
@RequestMapping("/editUser.do")
public class EditUserController {

	@Autowired
	private UserService userService;

	@RequestMapping(method=RequestMethod.GET)
	public String editUser() {
		return "editUser";
	}

	@ModelAttribute
	public void prepareUser(@RequestParam Long userId, Model model) {
		User user = userService.getUser(userId);
		model.addAttribute("user", user);
	}

	@InitBinder
	public void initBinder(WebDataBinder binder) {
		SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy");
		CustomDateEditor editor = new CustomDateEditor(format, false);
		binder.registerCustomEditor(Date.class, "registrationDate", editor);
	}

	@RequestMapping(method=RequestMethod.POST)
	public String onSubmit(@Valid @ModelAttribute User user, BindingResult br) {
		if (br.hasErrors()) {
			return "editUser";
		}

		userService.updateUser(user);
		return "redirect:userList.do";
	}
}

Unit test pro metodu editUser(), tedy příchod na formulář a vytažení uživatele z databáze, vypadá následovně:

	@Test
	public void testEditForm() throws Exception {
		User user = new User("pavel");
		user.setUserId(42L);
		expect(userService.getUser(42)).andReturn(user);
		replay(userService);

		mockMvc.perform(get("/editUser.do").param("userId", "42"))
			.andExpect(status().isOk())
			.andExpect(model().attribute("user", user))
			.andExpect(view().name("editUser"));

		verify(userService);
	}

Za zmínku stojí fluent API a nutnost dělat statické importy na celkem asi 3 třídy (nebo si je přidat do Eclipse jako Favorite stejně jako u Assert.* a EasyMock.*). Místo střední vrstvy je pooužitý mock z EasyMocku a ten je injectovaný do controlleru.

Aby unit test fungoval, je třeba nakonfigurovat třídu testu pomocí anotací a dodat aplikační kontext, který konfiguruje webovou vrstvu aplikace. Unit test třída vypadá takto:

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("appCtx-test.xml")
public class EditUserControllerTest {

	@Autowired
	private WebApplicationContext wac;

	@Autowired
	private UserService userService;

	private MockMvc mockMvc;

	@Before
	public void setup() {
		mockMvc = webAppContextSetup(wac).build();
		reset(userService);
	}

	// tests here...

}

Test odeslání formuláře, bindingu, validace a aktualizace uživatele na střední vrstvě:

	@Test
	public void testSubmitForm() throws Exception {
		User user = new User("pavel");
		user.setUserId(42L);
		expect(userService.getUser(42)).andReturn(user);
		Capture capture = new Capture<>();
		userService.updateUser(capture(capture));
		replay(userService);

		mockMvc.perform(post("/editUser.do")
					.param("userId", "42")
					.param("password", "secret")
					.param("registrationDate", "20.10.2013"))
				.andExpect(redirectedUrl("userList.do"));

		User updatedUser = capture.getValue();
		assertEquals("secret", updatedUser.getPassword());
		assertEquals("20.10.13", new SimpleDateFormat("dd.MM.yy").format(updatedUser.getRegistrationDate()));

		verify(userService);
	}

Stručný příklad testu i na selhání bindingu a validace nechávám v příloze článku. Celkově si myslím, že je to skvělý způsob, jak kvalitně udělat testy na Spring MVC vrstvu. Silně zvažujeme, jestli to stane i naším standardem pro všechny projekty, které děláme.

Vaše emailová adresa nebude zveřejněna

Komentáře

Děkujeme za váš komentář
Další
  • Martin Vich

    A nezkouseli jste pro funkcni testy krome Selenia Groovy frameworky Geb/Spock (http://www.gebish.org/) ? Vyhodu vidim v kompaktnejsim zapisu testu, nevyhodou je ze psani techto testu bude ukolem predevsim pro vyvojare. Slidy zde : http://qconlondon.com/dl/qcon-london-2013/slides/PeterNiederwieser_TamingFunctionalWebTestingWithSpockAndGeb.pdf

  • Vypadá to zajímavě. Ještě jsme nezkoušeli. Musíme se na to podívat. Tenhle článek je spíš o unit testech. Automatizovaných funkčních testů se samozřejmě nechceme vzdát.

  • Vojtěch Krása

    Tohle používáme taky, ty mock buildery se dají vycopypastovat a upravit i pro použití s CXF - stačí místo TestDispatcherServlet vytvářet CXFServlet.