Нет значения в пути JSON $.name, исключение: json не может быть нулевым или пустым, с использованием Mockmvc и Spring-boot

В духе изучения Spring-boot с помощью Spring Mock-Mvc и/или Mockito я создал небольшой API, который может усложниться позже, когда я узнаю больше.

Тема — «Песнь льда и пламени» или «Игра престолов». Пока у меня есть только один пакет, в котором можно добавлять, удалять и получать разные королевства при отправке запросов в "/Вестерос". Каждому королевству на данный момент требуется только свойство имени.

Я использую базу данных Neo4J.

Я загрузил код на github, вот ссылка https://github.com/darwin757/IceAndFire

Проблема: Порблема находится в моем классе KingdomControllerTest в методах addKingdomTest и updateKingdomTest.

    @Test
public void addKingdomTest() throws Exception {

    mockMvc.perform(post("/Westeros").contentType(MediaType.APPLICATION_JSON_UTF8).content("{\"name\":\"Dorne\"}")
            .accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isCreated()).andReturn();

    //This part of the test is not working
    mockMvc.perform(get("/Westeros/Dorne").contentType(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Dorne"));

}

@Test
public void updateKingdomTest() throws Exception {

    mockMvc.perform(post("/Westeros").contentType(MediaType.APPLICATION_JSON_UTF8).content("{\"name\":\"Dorne\"}")
            .accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isCreated());

    mockMvc.perform(put("/Westeros/Dorne").contentType(MediaType.APPLICATION_JSON_UTF8)
            .content("{\"name\":\"theReach\"}").accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk());

    //This Part of the test is not working
    mockMvc.perform(get("/Westeros/Dorne").contentType(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("the Reach"));

}

Как видите, когда я прошу API создать новое Королевство, он возвращает 201 isCreated или 200 isOK, но когда я отправляю запрос на получение, я получаю ответ «Нет значения в исключении пути JSON».

java.lang.AssertionError: No value at JSON path "$.name", exception: json can not be null or empty
at org.springframework.test.util.JsonPathExpectationsHelper.evaluateJsonPath(JsonPathExpectationsHelper.java:245)
at org.springframework.test.util.JsonPathExpectationsHelper.assertValue(JsonPathExpectationsHelper.java:99)
at org.springframework.test.web.servlet.result.JsonPathResultMatchers$2.match(JsonPathResultMatchers.java:100)
at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:171)
at com.example.Westeros.Kingdoms.KingdomControllerTest.addKingdomTest(KingdomControllerTest.java:95)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:678)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

Я очень новичок в Spring в целом, и я не нашел подходящих руководств по Spring Mock-Mvc или Mockito. Я понятия не имею, что не так, это мой синтаксис или мой API? любая помощь будет оценена.

Вот весь класс:

package com.example.Westeros.Kingdoms;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.stubbing.OngoingStubbing;
import org.springframework.beans.factory.annotation.Autowired;
import 
org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.mockito.Mockito.when;
import static org.junit.Assert.*;
import static org.mockito.Mockito.any;

import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;

//TODO Major refactor required to clean up this class and consider the 
testing strategy 
@RunWith(SpringRunner.class)
@SpringBootTest
public class KingdomControllerTest {

@Autowired
private WebApplicationContext context;

private MockMvc mockMvc;

@MockBean
private KingdomService kingdomServiceMock;

@Before
public void setup() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}

@Test
public void getAllKingdomsTest() throws Exception {

    List<Kingdom> kingdoms = setUpAListOfKingdoms();

    when(kingdomServiceMock.getAllKingdoms()).thenReturn(kingdoms);

    mockMvc.perform(get("/Westeros").accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(jsonPath("$[0].name").value("TheNorth"))
            .andExpect(jsonPath("$[1].name").value("TheRiverlands"));
}


@Test
public void getKingdomTest() throws Exception {

    Kingdom theNorth = setUpAKingdom("TheNorth");
    kingdomServiceMock.addKingdom(theNorth);

    when(kingdomServiceMock.getKingdom("TheNorth")).thenReturn(theNorth);

    mockMvc.perform(get("/Westeros/TheNorth")).andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(jsonPath("$.name").value("TheNorth"));
}

// FIXME This test is returning 201 isCreated,
// but if I perform a get after I get an assertion exception that the variable
// name is empty.
@Test
public void addKingdomTest() throws Exception {

    mockMvc.perform(post("/Westeros").contentType(MediaType.APPLICATION_JSON_UTF8).content("{\"name\":\"Dorne\"}")
            .accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isCreated()).andReturn();

    //This part of the test is not working
    mockMvc.perform(get("/Westeros/Dorne").contentType(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Dorne"));

}

@Test
public void updateKingdomTest() throws Exception {

    mockMvc.perform(post("/Westeros").contentType(MediaType.APPLICATION_JSON_UTF8).content("{\"name\":\"Dorne\"}")
            .accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isCreated());

    mockMvc.perform(put("/Westeros/Dorne").contentType(MediaType.APPLICATION_JSON_UTF8)
            .content("{\"name\":\"theReach\"}").accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk());

    //This Part of the test is not working
    mockMvc.perform(get("/Westeros/Dorne").contentType(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("the Reach"));

}

@Test
public void deleteKingdomTest() throws Exception {

    mockMvc.perform(post("/Westeros").contentType(MediaType.APPLICATION_JSON_UTF8).content("{\"name\":\"theVale\"}")
            .accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isCreated());

    mockMvc.perform(delete("Westeros/theVale").contentType(MediaType.APPLICATION_JSON_UTF8))
            .andExpect(status().isNotFound());

}

// FIXME refer to the KingdomController class, the method should be moved to
// another class more suited to it's purpose
@Test
public void deleteAlltest() {
}

private List<Kingdom> setUpAListOfKingdoms() {

    Kingdom theNorth = setUpAKingdom("TheNorth");
    Kingdom theRiverlands = setUpAKingdom("TheRiverlands");

    List<Kingdom> kingdoms = new ArrayList<Kingdom>();

    kingdoms.add(theNorth);
    kingdoms.add(theRiverlands);

    // FIXME wrong place for this code but I can't find another
    kingdomServiceMock.addKingdom(theNorth);
    kingdomServiceMock.addKingdom(theRiverlands);

    return kingdoms;

}

private Kingdom setUpAKingdom(String name) {

    Kingdom kingdom = new Kingdom(name);
    return kingdom;
}

}

Заранее спасибо.


person Darwin    schedule 25.10.2017    source источник
comment
Обычно я разделял тестирование своих сообщений на получение и размещение, обращаясь к репозиторию непосредственно в тестах, вместо того, чтобы создавать тестовые данные через mockMvc. Я не думаю, что это решит вашу проблему. Сбои могут указывать на то, что ваш get просто не работает. Вы пробовали это вне тестов?   -  person Plog    schedule 25.10.2017
comment
Я не знаю ваше тело ответа, но jsonPath($.name) неверен, либо измените на jsonPath(name), либо покажите свой объект ответа   -  person mrkernelpanic    schedule 25.10.2017
comment
@Plog У меня есть тест только для метода get и тест для метода getAll, и они работают нормально. Я просто не хотел тестировать метод Post, проверяя только ответ 201, поэтому я добавил туда еще один тест get, чтобы убедиться, что объект, который я намеревался, действительно создан.   -  person Darwin    schedule 25.10.2017
comment
Ах, теперь я вижу, что вы хотите вернуть список королевства. Таким образом, вы можете утверждать одно свойство одного элемента следующим образом: jsonPath(kingdomList.[0].name) т.е. я бы создал класс-оболочку с именем, например. KingdomList, который содержит Список‹Королевство›   -  person mrkernelpanic    schedule 25.10.2017


Ответы (1)


Хорошо, я проверил ваш github, и проблема в том, что вы издеваетесь над своим KingdomService:

@MockBean
private KingdomService kingdomServiceMock;

Но в этих неудачных тестах вы не заявляете о каком-либо поведении вызываемых имитируемых методов. Ответом по умолчанию для издеваемого класса является возврат null, поэтому метод KingdomService.getKingdom(name) здесь всегда возвращает null:

@RequestMapping("/Westeros/{name}")
    public Kingdom getKingdom(@PathVariable String name) {
        return kingdomService.getKingdom(name);
}

Вы, вероятно, захотите сделать больше интеграционного теста, и в этом случае я бы сказал, что вы не хотите издеваться над сервисом.

Поэтому в ваших тестах на получение, которые в настоящее время работают, вместо того, чтобы настраивать фиктивное поведение, вы должны автоматически подключить репозиторий и фактически добавить Королевства, для которых вы хотите протестировать, например:

@Autowired
KingdomRepository kingdomRepository;

@Test
@Transactional
    public void getAllKingdomsTest() throws Exception {

        List<Kingdom> kingdoms = setUpAListOfKingdoms();

        kingdomRepository.saveAll(kingdoms);
        kingdomRepository.flush();

        mockMvc.perform(get("/Westeros").accept(MediaType.APPLICATION_JSON_UTF8)).andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$[0].name").value("TheNorth"))
                .andExpect(jsonPath("$[1].name").value("TheRiverlands"));
    }

Аннотация @Transactional гарантирует, что ваши взаимодействия с базой данных откатываются в конце каждого теста.

EDIT: вы также должны убедиться, что ваши репозитории реализуют JpaRepository, а не PagingAndSortingRepository. Таким образом, вы можете вызвать метод .flush() в своем репозитории, чтобы гарантировать, что все ожидающие изменения в БД будут мгновенно сброшены в него.

person Plog    schedule 25.10.2017
comment
Спасибо, добрый сэр, что нашли время ответить. К сожалению, использование репозитория Autowired сломало мои тесты get и не исправило мои тесты post. Причина, по которой я использую Mock, заключается в том, что я хотел научиться тестировать фреймворк с помощью Mockmvc вместо использования, например, прямого JUnit с OkHttp, и все мои тесты должны выполняться изолированно в чистой базе данных и не влиять друг на друга. Я думаю, что мой главный вопрос будет заключаться в том, есть ли способ с MockMvc проверить, что элемент, который я создаю с помощью моего метода POST, действительно имеет правильные данные? или я должен просто быть доволен кодом ответа 201? - person Darwin; 26.10.2017
comment
Что именно сломалось при использовании вашего автоматического репозитория? Он должен работать. Кроме того, подход MockMvc не означает, что вам нужно издеваться над своими услугами. Он просто предоставляет способ имитации инфраструктуры Spring MVC, включая имитацию HTTP-запросов и ответов. Во всяком случае, я бы не стал использовать макеты в этом случае, поскольку тесты с использованием MockMvc уже являются скорее тестом полной интеграции для ваших конечных точек HTTP. Вы можете заставить свои тесты работать изолированно и не влиять друг на друга в БД, пометив свои тесты как @Transactional. Я обновил свой вопрос, чтобы включить это. - person Plog; 26.10.2017
comment
А что касается проверки того, создает ли ваш пост правильные данные. Да, вы можете проверить это, вызвав сообщение через mockMvc, а затем вызвав get непосредственно в вашем репозитории, чтобы проверить, что в БД есть объект, который вы только что создали. - person Plog; 26.10.2017
comment
@Darwin Я также понял, что вам, вероятно, следует использовать JpaRepository в качестве своих репозиториев вместо PagingAndSortingRepository. Таким образом, вы можете вызвать метод flush(), который вы можете вызывать в своих тестах, чтобы гарантировать, что все запросы на изменение БД будут немедленно сброшены. Это тоже должно помочь исправить ситуацию. Отредактированный ответ снова. - person Plog; 26.10.2017
comment
Спасибо за вашу непрерывную поддержку, теперь я понимаю намного больше о проблеме, когда я меняю свой KingdomRepository для реализации JpaRepository, я получаю java.lang.IllegalStateException: Failed to load ApplicationContext (я включил все необходимые зависимости maven и импорт) теперь у всего есть собственная ветка git. Сейчас я просто проверяю ответ 201/200 в тестах контроллера. - person Darwin; 27.10.2017