2

In the spirit of learning Spring-boot with Spring Mock-Mvc and/or Mockito, I have build a small API with the potential of getting complicated later when I learn more stuff.

The Theme is "A Song of Ice and Fire" or "Game Of Thrones". So far I only have one package where you can add, delete and get different kingdoms when you send requests to "/Westeros". Each Kingdom only require a name property for now.

The Database I am using is Neo4J.

I uploaded the code to github, here is the link https://github.com/darwin757/IceAndFire

The Problem: The Porblem is in my KingdomControllerTest class in the methods addKingdomTest and 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"));

}

As you see, when I ask the API to create a new Kingdom, it returns a 201 isCreated or 200 isOK, but when I send a get request I get back a "No value at JSON path exception"

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)

I am Very new to Spring in general, and I did not find proper guides on spring Mock-Mvc or Mockito. I have no idea what is wrong, is it my syntax or my API? any help would be appreciated.

Here is the entire Class:

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;
}

}

Thank you in advance.

fap
  • 663
  • 1
  • 5
  • 14
Darwin
  • 27
  • 1
  • 1
  • 5
  • I would usually split testing my gets from my posts and puts by accessing the repository directly in the tests instead of creating test data through the mockMvc. I don't think this will fix your issue though. The failings might indicate that your get just isn't working. Have you tried it outside of tests? – Plog Oct 25 '17 at 10:49
  • I dont know your response body, but jsonPath("$.name") is wrong, either change to jsonPath("name") or show your response entity – mrkernelpanic Oct 25 '17 at 10:54
  • @Plog I do have a test only for the get method, and a test for the getAll method, and they work fine. I just did not want to test the Post method by only checking for a 201 response that is why I added another get test there to make sure that the object I intended is actually created. – Darwin Oct 25 '17 at 10:55
  • Ah I now see that you want to return a List of Kingdom. So you can assert one property of one item like this: jsonPath("kingdomList.[0].name") i.e. I would create a wrapper class called e.g. KingdomList which contains the List – mrkernelpanic Oct 25 '17 at 10:59

1 Answers1

2

Ok I checked your github and the problem is that you are mocking your KingdomService:

@MockBean
private KingdomService kingdomServiceMock;

But in these failing tests you are not asserting any behaviour for the mocked methods that are called. The default response for a mocked class is to return null hence the kingdomService.getKingdom(name) method here returns null always:

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

You probably want to do more of an integration test in which case I would say you don't want to mock the service.

Therefore in your get tests that are currently working instead of setting up a mock behavior you should autowire the repository and actually add the Kingdoms you want to test for, e.g.:

@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"));
    }

The @Transactional annotation ensures that the interactions you have with the database are rolled back at the end of each test.

EDIT: You should also make sure your repositories are implementing JpaRepository not PagingAndSortingRepository. This way you can call the .flush() method on your repo to ensure all pending changes to the DB are instantly flushed to it.

Plog
  • 9,164
  • 5
  • 41
  • 66
  • Thank you good sir for taking the time to answer. Unfortunately, using the an Autowired repository broke my get tests and did not fixed my post tests. The reason I am using Mock is that I wanted to learn how to test a framework with Mockmvc instead of using direct JUnit with OkHttp for example, and all my tests should run in isolation on a clean database, and not affect one another. I think the my main question will be, Is there a way with MockMvc to test that the element I create through my POST method actually has the right data? or should I just be happy with the 201 response code? – Darwin Oct 26 '17 at 09:57
  • What broke exactly when using your autowired repository? It should work. Also the MockMvc approach doesn't mean you need to mock your services. It just provides a way of mocking the Spring MVC infrastructure including mocking HTTP requests and responses. If anything I wouldn't use mocks in this case since tests using MockMvc are already more of a full integration test for your HTTP endpoints. You can make you tests run in isolation and not affect each other in the DB by marking your tests as @Transactional. I've updated my question to include that. – Plog Oct 26 '17 at 11:21
  • And as for testing whether your post creates the right data. Yes, you can test this by calling the post via mockMvc then calling the get directly on your repository to check the DB has the entity you just created. – Plog Oct 26 '17 at 11:22
  • @Darwin I also realised you probably should be using JpaRepository as your repositories instead of PagingAndSortingRepository. This way you can call the flush() method which you can call in your tests to ensure all requests to change the DB are flushed immediately. This should help fix things too. Editted answer again. – Plog Oct 26 '17 at 11:31
  • Thanks for your continuos support, I understand a lot more about the problem now, when I change my KingdomRepository to implement JpaRepository I get a java.lang.IllegalStateException: Failed to load ApplicationContext (I did include all the necessary maven dependencies and imports) The whole thing has it's own git branch now. I am just checking for 201/200 response in the controller tests for now. – Darwin Oct 27 '17 at 13:00