I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE COURSE

1. Overview

We’ve been building out the REST API of our simple Reddit App for a while now – it’s time to get serious and start testing it.

And now that we finally switched to a simpler authentication mechanism, it’s easier to do so as well. We’re going to be using the powerful rest-assured library for all of these live tests.

2. Initial Setup

API tests need a user to run; in order to simplify running tests against the API, we’ll have a test user created beforehand – on application bootstrap:

@Component
public class Setup {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PreferenceRepository preferenceRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostConstruct
    private void createTestUser() {
        User userJohn = userRepository.findByUsername("john");
        if (userJohn == null) {
            userJohn = new User();
            userJohn.setUsername("john");
            userJohn.setPassword(passwordEncoder.encode("123"));
            userJohn.setAccessToken("token");
            userRepository.save(userJohn);
            final Preference pref = new Preference();
            pref.setTimezone(TimeZone.getDefault().getID());
            pref.setEmail("[email protected]");
            preferenceRepository.save(pref);
            userJohn.setPreference(pref);
            userRepository.save(userJohn);
        }
    }
}

Notice how Setup is a plain bean and we’re using the @PostConstruct annotation to hook in the actual setup logic.

3. Support for Live Tests

Before we start to actually write our tests, let’s first set up some basic supporting functionality we can then leverage.

We need things like authentication, URL paths, and maybe some JSON marhalling and unmarshalling capabilities:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
  classes = { TestConfig.class }, 
  loader = AnnotationConfigContextLoader.class)
public class AbstractLiveTest {
    public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

    @Autowired
    private CommonPaths commonPaths;

    protected String urlPrefix;

    protected ObjectMapper objectMapper = new ObjectMapper().setDateFormat(dateFormat);

    @Before
    public void setup() {
        urlPrefix = commonPaths.getServerRoot();
    }
    
    protected RequestSpecification givenAuth() {
        FormAuthConfig formConfig 
          = new FormAuthConfig(urlPrefix + "/j_spring_security_check", "username", "password");
        return RestAssured.given().auth().form("john", "123", formConfig);
    }

    protected RequestSpecification withRequestBody(RequestSpecification req, Object obj) 
      throws JsonProcessingException {
        return req.contentType(MediaType.APPLICATION_JSON_VALUE)
          .body(objectMapper.writeValueAsString(obj));
    }
}

We are just defining some simple helper methods and fields to make the actual testing easier:

  • givenAuth(): to perform the authentication
  • withRequestBody(): to send the JSON representation of an Object as the body of the HTTP request

And here is our simple bean – CommonPaths – providing a clean abstraction to the URLs of the system:

@Component
@PropertySource({ "classpath:web-${envTarget:local}.properties" })
public class CommonPaths {

    @Value("${http.protocol}")
    private String protocol;

    @Value("${http.port}")
    private String port;

    @Value("${http.host}")
    private String host;

    @Value("${http.address}")
    private String address;

    public String getServerRoot() {
        if (port.equals("80")) {
            return protocol + "://" + host + "/" + address;
        }
        return protocol + "://" + host + ":" + port + "/" + address;
    }
}

And the local version of the properties file: web-local.properties:

http.protocol=http
http.port=8080
http.host=localhost
http.address=reddit-scheduler

Finally, the very simple test Spring configuration:

@Configuration
@ComponentScan({ "org.baeldung.web.live" })
public class TestConfig {
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

4. Test the /scheduledPosts API

The first API we’re going to test is the /scheduledPosts API:

public class ScheduledPostLiveTest extends AbstractLiveTest {
    private static final String date = "2016-01-01 00:00";

    private Post createPost() throws ParseException, IOException {
        Post post = new Post();
        post.setTitle("test");
        post.setUrl("test.com");
        post.setSubreddit("test");
        post.setSubmissionDate(dateFormat.parse(date));

        Response response = withRequestBody(givenAuth(), post)
          .post(urlPrefix + "/api/scheduledPosts?date=" + date);
        
        return objectMapper.reader().forType(Post.class).readValue(response.asString());
    }
}

First, let’s test scheduling a new post:

@Test
public void whenScheduleANewPost_thenCreated() 
  throws ParseException, IOException {
    Post post = new Post();
    post.setTitle("test");
    post.setUrl("test.com");
    post.setSubreddit("test");
    post.setSubmissionDate(dateFormat.parse(date));

    Response response = withRequestBody(givenAuth(), post)
      .post(urlPrefix + "/api/scheduledPosts?date=" + date);

    assertEquals(201, response.statusCode());
    Post result = objectMapper.reader().forType(Post.class).readValue(response.asString());
    assertEquals(result.getUrl(), post.getUrl());
}

Next, let’s test retrieving all scheduled posts of a user:

@Test
public void whenGettingUserScheduledPosts_thenCorrect() 
  throws ParseException, IOException {
    createPost();

    Response response = givenAuth().get(urlPrefix + "/api/scheduledPosts?page=0");

    assertEquals(201, response.statusCode());
    assertTrue(response.as(List.class).size() > 0);
}

Next, let’s test editing a scheduled post:

@Test
public void whenUpdatingScheduledPost_thenUpdated() 
  throws ParseException, IOException {
    Post post = createPost();

    post.setTitle("new title");
    Response response = withRequestBody(givenAuth(), post).
      put(urlPrefix + "/api/scheduledPosts/" + post.getId() + "?date=" + date);

    assertEquals(200, response.statusCode());
    response = givenAuth().get(urlPrefix + "/api/scheduledPosts/" + post.getId());
    assertTrue(response.asString().contains(post.getTitle()));
}

Finally, let’s test the delete operation in the API:

@Test
public void whenDeletingScheduledPost_thenDeleted() 
  throws ParseException, IOException {
    Post post = createPost();
    Response response = givenAuth().delete(urlPrefix + "/api/scheduledPosts/" + post.getId());

    assertEquals(204, response.statusCode());
}

5. Test the /sites API

Next – let’s test the API publishing the Sites resources – the sites defined by a user:

public class MySitesLiveTest extends AbstractLiveTest {

    private Site createSite() throws ParseException, IOException {
        Site site = new Site("/feed/");
        site.setName("baeldung");
        
        Response response = withRequestBody(givenAuth(), site)
          .post(urlPrefix + "/sites");

        return objectMapper.reader().forType(Site.class).readValue(response.asString());
    }
}

Let’s test retrieving all the sites of the user:

@Test
public void whenGettingUserSites_thenCorrect() 
  throws ParseException, IOException {
    createSite();
    Response response = givenAuth().get(urlPrefix + "/sites");

    assertEquals(200, response.statusCode());
    assertTrue(response.as(List.class).size() > 0);
}

And also retrieving the articles of a site:

@Test
public void whenGettingSiteArticles_thenCorrect() 
  throws ParseException, IOException {
    Site site = createSite();
    Response response = givenAuth().get(urlPrefix + "/sites/articles?id=" + site.getId());

    assertEquals(200, response.statusCode());
    assertTrue(response.as(List.class).size() > 0);
}

Next, let’s test adding a new Site:

@Test
public void whenAddingNewSite_thenCorrect() 
  throws ParseException, IOException {
    Site site = createSite();

    Response response = givenAuth().get(urlPrefix + "/sites");
    assertTrue(response.asString().contains(site.getUrl()));
}

And deleting it:

@Test
public void whenDeletingSite_thenDeleted() throws ParseException, IOException {
    Site site = createSite();
    Response response = givenAuth().delete(urlPrefix + "/sites/" + site.getId());

    assertEquals(204, response.statusCode());
}

6. Test the /user/preferences API

Finally, let’s focus on the API exposing the preferences of the user.

First, let’s test getting user’s preferences:

@Test
public void whenGettingPrefernce_thenCorrect() {
    Response response = givenAuth().get(urlPrefix + "/user/preference");

    assertEquals(200, response.statusCode());
    assertTrue(response.as(Preference.class).getEmail().contains("john"));
}

And editing them:

@Test
public void whenUpdattingPrefernce_thenCorrect() 
  throws JsonProcessingException {
    Preference pref = givenAuth().get(urlPrefix + "/user/preference").as(Preference.class);
    pref.setEmail("[email protected]");
    Response response = withRequestBody(givenAuth(), pref).
      put(urlPrefix + "/user/preference/" + pref.getId());

    assertEquals(200, response.statusCode());
    response = givenAuth().get(urlPrefix + "/user/preference");
    assertEquals(response.as(Preference.class).getEmail(), pref.getEmail());
}

7. Conclusion

In this quick article we put together some basic testing for our REST API.

Nothing to fancy though – more advanced scenarios are needed – but this isn’t about perfection, it’s about progress and iterating in public.

I just announced the new Spring Boot 2 material, coming in REST With Spring:

>> CHECK OUT THE LESSONS