From 2723283640ce64dd626c6f1ceac01db6bb369a6a Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Mon, 12 Feb 2018 17:05:31 -0600 Subject: [PATCH] Add 'simplified' version --- README.adoc | 1 + pom.xml | 3 +- simplified/README.adoc | 223 ++++++++++++++++++ simplified/pom.xml | 34 +++ .../hateoas/examples/DatabaseLoader.java | 44 ++++ .../hateoas/examples/Employee.java | 68 ++++++ .../hateoas/examples/EmployeeController.java | 135 +++++++++++ .../hateoas/examples/EmployeeRepository.java | 26 ++ .../SpringHateoasSimplifiedApplication.java | 43 ++++ .../examples/EmployeeControllerTests.java | 78 ++++++ 10 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 simplified/README.adoc create mode 100644 simplified/pom.xml create mode 100644 simplified/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java create mode 100644 simplified/src/main/java/org/springframework/hateoas/examples/Employee.java create mode 100644 simplified/src/main/java/org/springframework/hateoas/examples/EmployeeController.java create mode 100644 simplified/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java create mode 100644 simplified/src/main/java/org/springframework/hateoas/examples/SpringHateoasSimplifiedApplication.java create mode 100644 simplified/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java diff --git a/README.adoc b/README.adoc index 87f013d..dea9c4d 100644 --- a/README.adoc +++ b/README.adoc @@ -12,6 +12,7 @@ We have separate folders for each of these: == Spring HATEOAS Modules * link:basics[Basics] - Poke and prod at a hypermedia-powered service from inside the code as well as externally using standard tools +* link:simplified[Simplified] - Use Spring HATEOAS in the simplest way possible. * link:api-evolution[API Evolution] - Upgrade an existing REST resource * link:hypermedia[Hypermedia] - Create hypermedia-driven REST resources, linking them together, and supporting older links. * link:affordances[Affordances] - Create richer hypermedia controls using more complex hypermedia formats diff --git a/pom.xml b/pom.xml index 79b6192..eb7c5a5 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ org.springframework.boot spring-boot-starter-parent - 2.0.0.M7 + 2.0.0.RC1 @@ -49,6 +49,7 @@ api-evolution hypermedia affordances + simplified diff --git a/simplified/README.adoc b/simplified/README.adoc new file mode 100644 index 0000000..02dbabe --- /dev/null +++ b/simplified/README.adoc @@ -0,0 +1,223 @@ += Spring HATEOAS - Basic Example + +This guides shows how to add Spring HATEOAS in the simplest way possible. Like the rest of these examples, it uses a payroll system. + +NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. + +== Defining Your Domain + +The cornerstone of any example is the domain object: + +[source,java] +---- +@Data +@Entity +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +class Employee { + + @Id @GeneratedValue + private Long id; + private String firstName; + private String lastName; + private String role; + + ... +} +---- + +This domain object includes: + +* `@Data` - Lombok annotation to define a mutable value object +* `@Entity` - JPA annotation to make the object storagable in a classic SQL engine (H2 in this example) +* `@NoArgsConstructor(PRIVATE)` - Lombok annotation to create an empty constructor call to appease Jackson, but which is private and not usable to our app's code. +* `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios + +== Accessing Data + +To experiment with something realistic, you need to access a real database. This example leverages H2, an embedded JPA datasource. +And while it's not a requirement for Spring HATEOAS, this example uses Spring Data JPA. + +Create a repository like this: + +[source,java] +---- +interface EmployeeRepository extends CrudRepository { +} +---- + +This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD) +operations. + +[[converting-entities-to-resources]] +== Converting Entities to Resources + +In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as details on _how_ to +retrieve and update that information. + +Spring HATEOAS defines a generic `Resource` container that lets you store any domain object (`Employee` in this example), and +add additional links. + +IMPORTANT: Spring HATEOAS's `Resource` and `Link` classes are *vendor neutral*. HAL is thrown around a lot, being the +default mediatype, but these classes can be used to render any mediatype. + +The following Spring MVC controller defines the application's routes, and hence is the source of links needed +in the hypermedia. + +NOTE: This guide assumes you already somewhat familiar with Spring MVC. + +[source,java] +---- +@RestController +class EmployeeController { + + private final EmployeeRepository repository; + + EmployeeController(EmployeeRepository repository) { + this.repository = repository; + } + + ... +} +---- + +This piece of code shows how the Spring MVC controller is wired with a copy of the `EmployeeRepository` through +constructor injection and marked as a *REST controller* thanks to the `@RestController` annotation. + +The route for the https://martinfowler.com/bliki/DDD_Aggregate.html[aggregate root] is shown below: + +[source,java] +---- +/** + * Look up all employees, and transform them into a REST collection resource. + * Then return them through Spring Web's {@link ResponseEntity} fluent API. + * + * NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different + * default accept header, which allows XML to get requested first, so "produces" + * forces it to HAL JSON for all clients. + */ +@GetMapping(value = "/employees", produces = MediaTypes.HAL_JSON_VALUE) +ResponseEntity>> findAll() { + + List> employees = StreamSupport.stream(repository.findAll().spliterator(), false) + .map(employee -> new Resource<>(employee, + linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), + linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) + .collect(Collectors.toList()); + + return ResponseEntity.ok( + new Resources<>(employees, + linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel())); +} +---- + +It retrieves a collection of `Employee` objects, streams through a Java 8 spliterator, and converts them into a collection +of `Resource` objects by using Spring HATEOAS's `linkTo` and `methodOn` helpers to build links. + +* The natural convention with REST endpoints is to serve a *self* link (denoted by the `.withSelfRel()` call). +* It's also useful for any single item resource to include a link back to the aggregate (denoted by the `.withRel("employees")`). + +The whole collection of single item resources is then wrapped in a Spring HATEOAS `Resources` type. + +NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's +own set of links, separate from the links of each member of the collection. That's why the whole +structure is `Resources>` and not `Resources`. + +To build a single resource, the `/employees/{id}` route is shown below: + +[source,java] +---- +/** + * Look up a single {@link Employee} and transform it into a REST resource. Then return it through + * Spring Web's {@link ResponseEntity} fluent API. + * + * See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument. + * + * @param id + */ +@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE) +ResponseEntity> findOne(@PathVariable long id) { + + return repository.findById(id) + .map(employee -> new Resource<>(employee, + linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), + linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); +} +---- + +This code is almost identical. It fetches a single item `Employee` from the database and that wraps up into a +`Resource` object with the same links, but that's it. No need to create a `Resources` object since is NOT a +collection. + +IMPORTANT: Does this look like duplicate code found in the aggregate root? Sures it does. That's why Spring HATEOAS + includes the ability to define a `ResourceAssembler`. It lets you define, in one place, all the links for a given + entity type. Then you can reuse it as needed in all relevant controller methods. It's been left out of this section + for the sake of simplicity. + +== Testing Hypermedia + +Nothing is complete without testing. Thanks to Spring Boot, it's easier than ever to test a Spring MVC controller, +including the generated hypermedia. + +The following is a bare bones "slice" test case: + +[source,java] +---- +@RunWith(SpringRunner.class) +@WebMvcTest(EmployeeController.class) +public class EmployeeControllerTests { + + @Autowired + private MockMvc mvc; + + @MockBean + private EmployeeRepository repository; + + ... +} +---- + +* `@RunWith(SpringRunner.class)` is needed to leverage Spring Boot's test annotations with JUnit. +* `@WebMvcTest(EmployeeController.class)` confines Spring Boot to only autoconfiguring Spring MVC components, and _only_ +this one controller, making it a very precise test case. +* `@Autowired MockMvc` gives us a handle on a Spring Mock tester. +* `@MockBean` flags `EmployeeRepository` as a test collaborator, since we don't plan on talking to a real database in this test case. + +With this structure, we can start crafting a test case! + +[source,java] +---- +@Test +public void getShouldFetchAHalDocument() throws Exception { + + given(repository.findAll()).willReturn( + Arrays.asList( + new Employee(1L,"Frodo", "Baggins", "ring bearer"), + new Employee(2L,"Bilbo", "Baggins", "burglar"))); + + mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) + ... +} +---- + +* At first, the test case uses Mockito's `given()` method to define the "given"s of the test. +* Next, it uses Spring Mock MVC's `mvc` to `perform()` a *GET /employees* call with an accept header of HAL's mediatype. +* As a courtesy, it uses the `.andDo(print())` to give us a complete print out of the whole thing on the console. +* Finally, it chains a whole series of assertions. +** Verify HTTP status is *200 OK*. +** Verify the response *Content-Type* header is also HAL's mediatype (with UTF-8 flavor). +** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`. +** And so forth... + +The rest of the assertions are commented out, but you can read it in the source code. + +NOTE: This is not the only way to assert the results. See Spring Framework reference docs and Spring HATEOAS +test cases for more examples. + +For the next step in Spring HATEOAS, you may wish to read link:../api-evolution[Spring HATEOAS - API Evolution Example]. \ No newline at end of file diff --git a/simplified/pom.xml b/simplified/pom.xml new file mode 100644 index 0000000..c19c4e0 --- /dev/null +++ b/simplified/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + spring-hateoas-examples-simplified + Spring HATEOAS - Examples - Simplified + jar + + + org.springframework.hateoas.examples + spring-hateoas-examples + 1.0.0.BUILD-SNAPSHOT + + + + + org.springframework.hateoas.examples + commons + 1.0.0.BUILD-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/simplified/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java b/simplified/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java new file mode 100644 index 0000000..9583481 --- /dev/null +++ b/simplified/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.examples; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +/** + * Pre-load some data using a Spring Boot {@link CommandLineRunner}. + * + * @author Greg Turnquist + */ +@Component +class DatabaseLoader { + + /** + * Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run + * only after the app is operational, the database will be up. + * + * @param repository + */ + @Bean + CommandLineRunner init(EmployeeRepository repository) { + return args -> { + repository.save(new Employee("Frodo", "Baggins", "ring bearer")); + repository.save(new Employee("Bilbo", "Baggins", "burglar")); + }; + } + +} diff --git a/simplified/src/main/java/org/springframework/hateoas/examples/Employee.java b/simplified/src/main/java/org/springframework/hateoas/examples/Employee.java new file mode 100644 index 0000000..e63ee24 --- /dev/null +++ b/simplified/src/main/java/org/springframework/hateoas/examples/Employee.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.examples; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +/** + * Domain object representing a company employee. Project Lombok keeps actual code at a minimum. + * + * {@code @Data} - Generates getters, setters, toString, hash, and equals functions + * {@code @Entity} - JPA annotation to flag this class for DB persistence + * {@code @NoArgsConstructor} - Create a constructor with no args to support JPA + * {@code @AllArgsConstructor} - Create a constructor with all args to support testing + * + * {@code @JsonIgnoreProperties(ignoreUnknow=true)} + * When converting JSON to Java, ignore any unrecognized attributes. This is critical for REST because it + * encourages adding new fields in later versions that won't break. It also allows things like _links to be + * ignore as well, meaning HAL documents can be fetched and later posted to the server without adjustment. + * + * + * @author Greg Turnquist + */ +@Data +@Entity +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +class Employee { + + @Id @GeneratedValue + private Long id; + private String firstName; + private String lastName; + private String role; + + /** + * Useful constructor when id is not yet known. + * + * @param firstName + * @param lastName + * @param role + */ + Employee(String firstName, String lastName, String role) { + + this.firstName = firstName; + this.lastName = lastName; + this.role = role; + } +} diff --git a/simplified/src/main/java/org/springframework/hateoas/examples/EmployeeController.java b/simplified/src/main/java/org/springframework/hateoas/examples/EmployeeController.java new file mode 100644 index 0000000..81ab6a1 --- /dev/null +++ b/simplified/src/main/java/org/springframework/hateoas/examples/EmployeeController.java @@ -0,0 +1,135 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.examples; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.springframework.hateoas.Link; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +/** + * Spring Web {@link RestController} used to generate a REST API. + * + * @author Greg Turnquist + */ +@RestController +class EmployeeController { + + private final EmployeeRepository repository; + + EmployeeController(EmployeeRepository repository) { + this.repository = repository; + } + + /** + * Look up all employees, and transform them into a REST collection resource. + * Then return them through Spring Web's {@link ResponseEntity} fluent API. + * + * NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different + * default accept header, which allows XML to get requested first, so "produces" + * forces it to HAL JSON for all clients. + */ + @GetMapping(value = "/employees", produces = MediaTypes.HAL_JSON_VALUE) + ResponseEntity>> findAll() { + + List> employees = StreamSupport.stream(repository.findAll().spliterator(), false) + .map(employee -> new Resource<>(employee, + linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), + linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) + .collect(Collectors.toList()); + + return ResponseEntity.ok( + new Resources<>(employees, + linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel())); + } + + @PostMapping("/employees") + ResponseEntity newEmployee(@RequestBody Employee employee) { + + try { + Employee savedEmployee = repository.save(employee); + + Resource employeeResource = new Resource<>(savedEmployee, + linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel()); + + return ResponseEntity + .created(new URI(employeeResource.getRequiredLink(Link.REL_SELF).getHref())) + .body(employeeResource); + } catch (URISyntaxException e) { + return ResponseEntity.badRequest().body("Unable to create " + employee); + } + } + + /** + * Look up a single {@link Employee} and transform it into a REST resource. Then return it through + * Spring Web's {@link ResponseEntity} fluent API. + * + * See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument. + * + * @param id + */ + @GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE) + ResponseEntity> findOne(@PathVariable long id) { + + return repository.findById(id) + .map(employee -> new Resource<>(employee, + linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), + linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * Update existing employee then return a Location header. + * + * @param employee + * @param id + * @return + */ + @PutMapping("/employees/{id}") + ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable long id) { + + Employee employeeToUpdate = employee; + employeeToUpdate.setId(id); + repository.save(employeeToUpdate); + + Link newlyCreatedLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel(); + + try { + return ResponseEntity.noContent() + .location(new URI(newlyCreatedLink.getHref())) + .build(); + } catch (URISyntaxException e) { + return ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate); + } + } + +} diff --git a/simplified/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java b/simplified/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java new file mode 100644 index 0000000..61ec4c8 --- /dev/null +++ b/simplified/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.examples; + +import org.springframework.data.repository.CrudRepository; + +/** + * A simple Spring Data {@link CrudRepository} for storing {@link Employee}s. + * + * @author Greg Turnquist + */ +interface EmployeeRepository extends CrudRepository { +} diff --git a/simplified/src/main/java/org/springframework/hateoas/examples/SpringHateoasSimplifiedApplication.java b/simplified/src/main/java/org/springframework/hateoas/examples/SpringHateoasSimplifiedApplication.java new file mode 100644 index 0000000..8df9ea4 --- /dev/null +++ b/simplified/src/main/java/org/springframework/hateoas/examples/SpringHateoasSimplifiedApplication.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.hateoas.examples; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.hateoas.core.EvoInflectorRelProvider; + +/** + * @author Greg Turnquist + */ +@SpringBootApplication +public class SpringHateoasSimplifiedApplication { + + public static void main(String... args) { + SpringApplication.run(SpringHateoasSimplifiedApplication.class); + } + + /** + * Format embedded collections by pluralizing the resource's type. + * + * @return + */ + @Bean + EvoInflectorRelProvider relProvider() { + return new EvoInflectorRelProvider(); + } +} diff --git a/simplified/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java b/simplified/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java new file mode 100644 index 0000000..7292335 --- /dev/null +++ b/simplified/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.hateoas.examples; + +import static org.hamcrest.CoreMatchers.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +/** + * How to test the hypermedia-based {@link EmployeeController} with everything else mocked out. + * + * @author Greg Turnquist + */ +@RunWith(SpringRunner.class) +@WebMvcTest(EmployeeController.class) +public class EmployeeControllerTests { + + @Autowired + private MockMvc mvc; + + @MockBean + private EmployeeRepository repository; + + @Test + public void getShouldFetchAHalDocument() throws Exception { + + given(repository.findAll()).willReturn( + Arrays.asList( + new Employee(1L,"Frodo", "Baggins", "ring bearer"), + new Employee(2L,"Bilbo", "Baggins", "burglar"))); + + mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE)) + .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) + .andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo"))) + .andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins"))) + .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) + .andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1"))) + .andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees"))) + .andExpect(jsonPath("$._embedded.employees[1].id", is(2))) + .andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo"))) + .andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins"))) + .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) + .andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2"))) + .andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees"))) + .andExpect(jsonPath("$._links.self.href", is("http://localhost/employees"))) + .andReturn(); + } +}