Spring, MockMvc i @Scope("session")

To jeden z tych postów, które powstały, żeby nikt mi nie zarzucił, że blog nie żyje. Bo przecież postuję przynajmniej raz w roku! A poza tym może się komuś przyda.

Historia jakich wiele, stworzyliśmy sobie kontroler w Springu, a w tym kontrolerze jest użyty bean.

@RestController
@Scope(value="session")
public class SomeController {
@Autowired private StateBean state; @RequestMapping("/state") public String getState() { return state.name + " " + state.value; } }

Ale to nie jest taki zwykły bean. Ten nasz ma session scope. Dla niespringujących tłumaczę, beany z takim scope są powiązane z sesją użytkownika. To znaczy każdy użytkownik, który będzie korzystał z naszego serwisu będzie miał na wstępie przydzielony swój własny bean niczym opiekuna na wycieczce po Korei Północnej i przy każdej jego akcji będzie wykorzystywana ta sama instancja beana.

A tak wygląda kod tej nieszczęsnej fasolki:

@Component
@Scope(value="session")
public class StateBean {
public String name = "default"; public int value = 0; }

To teraz może testy (zaraz ktoś wyciągnie widły z napisem TDD, że testy powinny być najpierw, takim czytelnikom polecam czytać fragmenty posta w preferowanej przez siebie kolejności). Pierwsze podejście do przetestowania może wyglądać na przykład tak:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Autowired private WebApplicationContext context; @Autowired private StateBean state; @Test public void testState() throws Exception { state.name = "toggled"; state.value = 9; MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build(); MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/state")).andReturn(); Assert.assertEquals("toggled 9", result.getResponse().getContentAsString()); } }

Wygląda logicznie? Wszczepiamy beana, coś w nim grzebiemy i liczymy, że Spring w swojej nieskończonej mądrości wszczepi tego samego beana w nasze zapytanie. Oczywiście gdyby tak było to nie miałbym po co pisać tego posta (co mogło by zagrozić utrzymaniu mojej aktywności minimum raz na rok).

Jedna z metod, która może człowiekowi (w tej roli ja) przyjść do głowy to stworzenie własnej sesji i wsadzenie do niej beana otrzymanego od Springa, a potem wrzucenie do zapytania. To wymaga sprawdzenia pod jakimi nazwami trzymane są beany w sesji. Ogólnie śliska sprawa, bo trochę za bardzo bazuje na wewnętrznej implementacji. Twórcy Springa mogą pewnego dnia zdecydować, że będą stosować inną konwencję w nazwach i będzie problem po podbiciu wersji.

To może zastanówmy się, skąd Spring w ogóle bierze tego beana. Okazuje się, że tworzy sobie dla każdego testu osobną sesję. I jest na tyle uprzejmy, że nam ją wszczepi jeśli go o to poprosimy. I tak oto działający kod prezentuje się tak:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Autowired private WebApplicationContext context; @Autowired private StateBean state; @Autowired private MockHttpSession session;
@Test public void testState() throws Exception { state.name = "toggled"; state.value = 9; MockMvc mvc = MockMvcBuilders.webAppContextSetup(context).build(); MvcResult result = mvc.perform(MockMvcRequestBuilders.get("/state").session(session)).andReturn(); Assert.assertEquals("toggled 9", result.getResponse().getContentAsString()); } }

I to wszystko bez czarów i bez hacków.