diff --git a/Step23.md b/Step23.md new file mode 100644 index 0000000..bf8640f --- /dev/null +++ b/Step23.md @@ -0,0 +1,8 @@ +##What You Will Learn during this Step: +- Lets do some cleanup + - Move SomeBean to a different class and call it WelcomeController + +## Exercises +- Test and make sure everything is working fine + +## Files List \ No newline at end of file diff --git a/Step24.md b/Step24.md new file mode 100644 index 0000000..b7692f8 --- /dev/null +++ b/Step24.md @@ -0,0 +1,859 @@ +##What You Will Learn during this Step: +- Write a Unit Test for retrieving a specific question from a survey. +- Different between Unit Test and Integration Test +- Basics of Mocking +- MockMvc framework +- @MockBean + +## Useful Snippets and References +First Snippet +``` +package com.in28minutes.springboot.controller; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +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.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.request.MockMvcRequestBuilders; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.service.SurveyService; + +@RunWith(SpringRunner.class) +@WebMvcTest(SurveyController.class) +public class SurveyControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private SurveyService service; + + @Test + public void retrieveQuestion() throws Exception { + + Question mockQuestion = new Question("Question1", "First Alphabet", + "A", Arrays.asList("A", "B", "C", "D")); + + when(service.retrieveQuestion(anyString(), anyString())).thenReturn( + mockQuestion); + + MvcResult result = mvc + .perform( + MockMvcRequestBuilders.get( + "/surveys/Survey1/questions/1").accept( + MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + String expected = "{id:Question1,description:First Alphabet,correctAnswer:A,options:[A,B,C,D]}"; + + JSONAssert.assertEquals(expected, result.getResponse() + .getContentAsString(), false); + + } +} +``` + +## Exercises +- Write unit test for retrieve all questions for a survey + +## Files List +### /pom.xml +``` + + 4.0.0 + com.in28minutes + springboot-for-beginners-example + 0.0.1-SNAPSHOT + Your First Spring Boot Example + jar + + + org.springframework.boot + spring-boot-starter-parent + 1.4.0.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + + + + org.springframework.boot + spring-boot-starter-data-rest + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.data + spring-data-rest-hal-browser + + + + org.springframework.boot + spring-boot-devtools + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + 1.8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + +``` +### /src/main/java/com/in28minutes/springboot/Application.java +``` +package com.in28minutes.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + + ApplicationContext ctx = SpringApplication.run(Application.class, args); + } + +} +``` +### /src/main/java/com/in28minutes/springboot/configuration/BasicConfiguration.java +``` +package com.in28minutes.springboot.configuration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties("basic") +public class BasicConfiguration { + private boolean value; + private String message; + private int number; + + public boolean isValue() { + return value; + } + + public void setValue(boolean value) { + this.value = value; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + +} +``` +### /src/main/java/com/in28minutes/springboot/controller/SurveyController.java +``` +package com.in28minutes.springboot.controller; + +import java.net.URI; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +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.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.service.SurveyService; + +@RestController +class SurveyController { + @Autowired + private SurveyService surveyService; + + @GetMapping("/surveys/{surveyId}/questions") + public List retrieveQuestions(@PathVariable String surveyId) { + return surveyService.retrieveQuestions(surveyId); + } + + @GetMapping(path = "/surveys/{surveyId}/questions/{questionId}") + public Question retrieveQuestion(@PathVariable String surveyId, + @PathVariable String questionId) { + return surveyService.retrieveQuestion(surveyId, questionId); + } + + @PostMapping("/surveys/{surveyId}/questions") + ResponseEntity add(@PathVariable String surveyId, + @RequestBody Question question) { + + Question createdTodo = surveyService.addQuestion(surveyId, question); + + if (createdTodo == null) { + return ResponseEntity.noContent().build(); + } + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}").buildAndExpand(createdTodo.getId()).toUri(); + + return ResponseEntity.created(location).build(); + + } + +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/User.java +``` +package com.in28minutes.springboot.jpa; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name;// Not perfect!! Should be a proper object! + private String role;// Not perfect!! An enum should be a better choice! + + protected User() { + } + + public User(String name, String role) { + super(); + this.name = name; + this.role = role; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getRole() { + return role; + } + + @Override + public String toString() { + return String.format("User [id=%s, name=%s, role=%s]", id, name, role); + } + +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/UserCommandLineRunner.java +``` +package com.in28minutes.springboot.jpa; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class UserCommandLineRunner implements CommandLineRunner { + + private static final Logger log = LoggerFactory + .getLogger(UserCommandLineRunner.class); + + @Autowired + private UserRepository repository; + + @Override + public void run(String... args) { + // save a couple of customers + repository.save(new User("Ranga", "Admin")); + repository.save(new User("Ravi", "User")); + repository.save(new User("Satish", "Admin")); + repository.save(new User("Raghu", "User")); + + log.info("-------------------------------"); + log.info("Finding all users"); + log.info("-------------------------------"); + for (User user : repository.findAll()) { + log.info(user.toString()); + } + + log.info("-------------------------------"); + log.info("Finding user with id 1"); + log.info("-------------------------------"); + User user = repository.findOne(1L); + log.info(user.toString()); + + log.info("-------------------------------"); + log.info("Finding all Admins"); + log.info("-------------------------------"); + for (User admin : repository.findByRole("Admin")) { + log.info(admin.toString()); + // Do something... + } + } + +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/UserRepository.java +``` +package com.in28minutes.springboot.jpa; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +public interface UserRepository extends CrudRepository { + List findByRole(String description); +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/UserRestRepository.java +``` +package com.in28minutes.springboot.jpa; + +import java.util.List; + +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(collectionResourceRel = "users", path = "users") +public interface UserRestRepository extends +PagingAndSortingRepository { + List findByRole(@Param("role") String role); +} +``` +### /src/main/java/com/in28minutes/springboot/model/Question.java +``` +package com.in28minutes.springboot.model; + +import java.util.List; + +public class Question { + private String id; + private String description; + private String correctAnswer; + private List options; + + // Needed by Caused by: com.fasterxml.jackson.databind.JsonMappingException: + // Can not construct instance of com.in28minutes.springboot.model.Question: + // no suitable constructor found, can not deserialize from Object value + // (missing default constructor or creator, or perhaps need to add/enable + // type information?) + public Question() { + + } + + public Question(String id, String description, String correctAnswer, + List options) { + super(); + this.id = id; + this.description = description; + this.correctAnswer = correctAnswer; + this.options = options; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public String getCorrectAnswer() { + return correctAnswer; + } + + public List getOptions() { + return options; + } + + @Override + public String toString() { + return String + .format("Question [id=%s, description=%s, correctAnswer=%s, options=%s]", + id, description, correctAnswer, options); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (id == null ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Question other = (Question) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + +} +``` +### /src/main/java/com/in28minutes/springboot/model/Survey.java +``` +package com.in28minutes.springboot.model; + +import java.util.List; + +public class Survey { + private String id; + private String title; + private String description; + private List questions; + + public Survey(String id, String title, String description, + List questions) { + super(); + this.id = id; + this.title = title; + this.description = description; + this.questions = questions; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public List getQuestions() { + return questions; + } + + @Override + public String toString() { + return String.format( + "Survey [id=%s, title=%s, description=%s, questions=%s]", id, + title, description, questions); + } + +} +``` +### /src/main/java/com/in28minutes/springboot/service/SurveyService.java +``` +package com.in28minutes.springboot.service; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.model.Survey; + +@Component +public class SurveyService { + private static List surveys = new ArrayList<>(); + static { + Question question1 = new Question("Question1", + "Largest Country in the World", "Russia", Arrays.asList( + "India", "Russia", "United States", "China")); + Question question2 = new Question("Question2", + "Most Populus Country in the World", "China", Arrays.asList( + "India", "Russia", "United States", "China")); + Question question3 = new Question("Question3", + "Highest GDP in the World", "United States", Arrays.asList( + "India", "Russia", "United States", "China")); + Question question4 = new Question("Question4", + "Second largest english speaking country", "India", + Arrays.asList("India", "Russia", "United States", "China")); + + List questions = new ArrayList<>(Arrays.asList(question1, + question2, question3, question4)); + + Survey survey = new Survey("Survey1", "My Favorite Survey", + "Description of the Survey", questions); + + surveys.add(survey); + } + + public List retrieveAllSurveys() { + return surveys; + } + + public Survey retrieveSurvey(String surveyId) { + for (Survey survey : surveys) { + if (survey.getId().equals(surveyId)) { + return survey; + } + } + return null; + } + + public List retrieveQuestions(String surveyId) { + Survey survey = retrieveSurvey(surveyId); + + if (survey == null) { + return null; + } + + return survey.getQuestions(); + } + + public Question retrieveQuestion(String surveyId, String questionId) { + Survey survey = retrieveSurvey(surveyId); + + if (survey == null) { + return null; + } + + for (Question question : survey.getQuestions()) { + if (question.getId().equals(questionId)) { + return question; + } + } + + return null; + } + + private SecureRandom random = new SecureRandom(); + + public Question addQuestion(String surveyId, Question question) { + Survey survey = retrieveSurvey(surveyId); + + if (survey == null) { + return null; + } + + String randomId = new BigInteger(130, random).toString(32); + question.setId(randomId); + + survey.getQuestions().add(question); + + return question; + } +} +``` +### /src/main/java/com/in28minutes/springboot/WelcomeController.java +``` +package com.in28minutes.springboot; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.in28minutes.springboot.configuration.BasicConfiguration; + +@RestController +public class WelcomeController { + + @Autowired + private SomeDependency someDependency; + + @Autowired + private BasicConfiguration configuration; + + @RequestMapping("/") + public String index() { + return someDependency.getSomething(); + } + + @RequestMapping("/dynamic-configuration") + public Map dynamicConfiguration() { + // Not the best practice to use a map to store differnt types! + Map map = new HashMap(); + map.put("message", configuration.getMessage()); + map.put("number", configuration.getNumber()); + map.put("key", configuration.isValue()); + return map; + } + +} + +@Component +class SomeDependency { + + @Value("${welcome.message}") + private String welcomeMessage; + + public String getSomething() { + return welcomeMessage; + } + +} +``` +### /src/main/resources/application.properties +``` +logging.level.org.springframework: INFO +app.name: In28Minutes +app.description: ${app.name} is your first Spring Boot application Properties +welcome.message: Welcome to your first Spring Boot application +basic.value: true +basic.message: Dynamic Message +basic.number: 123 +``` +### /src/main/resources/application.yaml +``` +logging: + level: + org.springframework: DEBUG + +app: + name: In28Minutes + description: ${app.name} is your first Spring Boot application + +welcome: + message: Welcome to your first Spring Boot app! + +basic: + value: true + message: Dynamic Message YAML + number: 100 +``` +### /src/test/java/com/in28minutes/springboot/controller/SurveyControllerIT.java +``` +package com.in28minutes.springboot.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import com.in28minutes.springboot.Application; +import com.in28minutes.springboot.model.Question; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class SurveyControllerIT { + + @LocalServerPort + private int port; + + private TestRestTemplate template = new TestRestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + + @Before + public void setupJSONAcceptType() { + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + } + + @Test + public void retrieveTodo() throws Exception { + + String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia,options:[India,Russia,United States,China]}"; + + ResponseEntity response = template.exchange( + createUrl("/surveys/Survey1/questions/Question1"), + HttpMethod.GET, new HttpEntity("DUMMY_DOESNT_MATTER", + headers), String.class); + + JSONAssert.assertEquals(expected, response.getBody(), false); + } + + @Test + public void retrieveTodos() throws Exception { + ResponseEntity> response = template.exchange( + createUrl("/surveys/Survey1/questions/"), HttpMethod.GET, + new HttpEntity("DUMMY_DOESNT_MATTER", headers), + new ParameterizedTypeReference>() { + }); + + Question sampleQuestion = new Question("Question1", + "Largest Country in the World", "Russia", Arrays.asList( + "India", "Russia", "United States", "China")); + + assertTrue(response.getBody().contains(sampleQuestion)); + } + + @Test + public void addTodo() throws Exception { + Question question = new Question("DOESN'T MATTER", "Smallest Number", + "1", Arrays.asList("1", "2", "3", "4")); + + ResponseEntity response = template.exchange( + createUrl("/surveys/Survey1/questions/"), HttpMethod.POST, + new HttpEntity(question, headers), String.class); + + assertThat(response.getHeaders().get(HttpHeaders.LOCATION).get(0), + containsString("/surveys/Survey1/questions/")); + } + + private String createUrl(String uri) { + return "http://localhost:" + port + uri; + } + +} +``` +### /src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java +``` +package com.in28minutes.springboot.controller; + +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +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.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.request.MockMvcRequestBuilders; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.service.SurveyService; + +@RunWith(SpringRunner.class) +@WebMvcTest(SurveyController.class) +public class SurveyControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private SurveyService service; + + @Test + public void retrieveQuestion() throws Exception { + + Question mockQuestion = new Question("Question1", "First Alphabet", + "A", Arrays.asList("A", "B", "C", "D")); + + when(service.retrieveQuestion(anyString(), anyString())).thenReturn( + mockQuestion); + + MvcResult result = mvc + .perform( + MockMvcRequestBuilders.get( + "/surveys/Survey1/questions/1").accept( + MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + String expected = "{id:Question1,description:First Alphabet,correctAnswer:A,options:[A,B,C,D]}"; + + JSONAssert.assertEquals(expected, result.getResponse() + .getContentAsString(), false); + + } +} +``` diff --git a/Step25.md b/Step25.md new file mode 100644 index 0000000..4ce3ed6 --- /dev/null +++ b/Step25.md @@ -0,0 +1,896 @@ +##What You Will Learn during this Step: +- Exercise from previous step +- Unit test for createTodo + +## Useful Snippets and References +First Snippet +``` + @Test + public void retrieveTodos() throws Exception { + List mockList = Arrays.asList( + new Question("Question1", "First Alphabet", "A", Arrays.asList( + "A", "B", "C", "D")), + new Question("Question2", "Last Alphabet", "Z", Arrays.asList( + "A", "X", "Y", "Z"))); + + when(service.retrieveQuestions(anyString())).thenReturn(mockList); + + MvcResult result = mvc + .perform( + MockMvcRequestBuilders + .get("/surveys/Survey1/questions").accept( + MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + String expected = "[" + + "{id:Question1,description:First Alphabet,correctAnswer:A,options:[A,B,C,D]}," + + "{id:Question2,description:Last Alphabet,correctAnswer:Z,options:[A,X,Y,Z]}" + + "]"; + + JSONAssert.assertEquals(expected, result.getResponse() + .getContentAsString(), false); + } +``` +Second Snippet +``` + @Test + public void createTodo() throws Exception { + Question mockQuestion = new Question("1", "Smallest Number", "1", + Arrays.asList("1", "2", "3", "4")); + + String question = "{\"description\":\"Smallest Number\",\"correctAnswer\":\"1\",\"options\":[\"1\",\"2\",\"3\",\"4\"]}"; + + when(service.addQuestion(anyString(), any(Question.class))).thenReturn( + mockQuestion); + + mvc.perform( + MockMvcRequestBuilders.post("/surveys/Survey1/questions") + .content(question) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect( + header().string("location", + containsString("/surveys/Survey1/questions/1"))); + } +``` + +## Exercises + +## Files List +### /pom.xml +``` + + 4.0.0 + com.in28minutes + springboot-for-beginners-example + 0.0.1-SNAPSHOT + Your First Spring Boot Example + jar + + + org.springframework.boot + spring-boot-starter-parent + 1.4.0.RELEASE + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + + + + org.springframework.boot + spring-boot-starter-data-rest + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.data + spring-data-rest-hal-browser + + + + org.springframework.boot + spring-boot-devtools + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + 1.8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + +``` +### /src/main/java/com/in28minutes/springboot/Application.java +``` +package com.in28minutes.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + + ApplicationContext ctx = SpringApplication.run(Application.class, args); + } + +} +``` +### /src/main/java/com/in28minutes/springboot/configuration/BasicConfiguration.java +``` +package com.in28minutes.springboot.configuration; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties("basic") +public class BasicConfiguration { + private boolean value; + private String message; + private int number; + + public boolean isValue() { + return value; + } + + public void setValue(boolean value) { + this.value = value; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getNumber() { + return number; + } + + public void setNumber(int number) { + this.number = number; + } + +} +``` +### /src/main/java/com/in28minutes/springboot/controller/SurveyController.java +``` +package com.in28minutes.springboot.controller; + +import java.net.URI; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +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.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.service.SurveyService; + +@RestController +class SurveyController { + @Autowired + private SurveyService surveyService; + + @GetMapping("/surveys/{surveyId}/questions") + public List retrieveQuestions(@PathVariable String surveyId) { + return surveyService.retrieveQuestions(surveyId); + } + + @GetMapping(path = "/surveys/{surveyId}/questions/{questionId}") + public Question retrieveQuestion(@PathVariable String surveyId, + @PathVariable String questionId) { + return surveyService.retrieveQuestion(surveyId, questionId); + } + + @PostMapping("/surveys/{surveyId}/questions") + ResponseEntity add(@PathVariable String surveyId, + @RequestBody Question question) { + + Question createdTodo = surveyService.addQuestion(surveyId, question); + + if (createdTodo == null) { + return ResponseEntity.noContent().build(); + } + + URI location = ServletUriComponentsBuilder.fromCurrentRequest() + .path("/{id}").buildAndExpand(createdTodo.getId()).toUri(); + + return ResponseEntity.created(location).build(); + + } + +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/User.java +``` +package com.in28minutes.springboot.jpa; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name;// Not perfect!! Should be a proper object! + private String role;// Not perfect!! An enum should be a better choice! + + protected User() { + } + + public User(String name, String role) { + super(); + this.name = name; + this.role = role; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getRole() { + return role; + } + + @Override + public String toString() { + return String.format("User [id=%s, name=%s, role=%s]", id, name, role); + } + +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/UserCommandLineRunner.java +``` +package com.in28minutes.springboot.jpa; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class UserCommandLineRunner implements CommandLineRunner { + + private static final Logger log = LoggerFactory + .getLogger(UserCommandLineRunner.class); + + @Autowired + private UserRepository repository; + + @Override + public void run(String... args) { + // save a couple of customers + repository.save(new User("Ranga", "Admin")); + repository.save(new User("Ravi", "User")); + repository.save(new User("Satish", "Admin")); + repository.save(new User("Raghu", "User")); + + log.info("-------------------------------"); + log.info("Finding all users"); + log.info("-------------------------------"); + for (User user : repository.findAll()) { + log.info(user.toString()); + } + + log.info("-------------------------------"); + log.info("Finding user with id 1"); + log.info("-------------------------------"); + User user = repository.findOne(1L); + log.info(user.toString()); + + log.info("-------------------------------"); + log.info("Finding all Admins"); + log.info("-------------------------------"); + for (User admin : repository.findByRole("Admin")) { + log.info(admin.toString()); + // Do something... + } + } + +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/UserRepository.java +``` +package com.in28minutes.springboot.jpa; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +public interface UserRepository extends CrudRepository { + List findByRole(String description); +} +``` +### /src/main/java/com/in28minutes/springboot/jpa/UserRestRepository.java +``` +package com.in28minutes.springboot.jpa; + +import java.util.List; + +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(collectionResourceRel = "users", path = "users") +public interface UserRestRepository extends +PagingAndSortingRepository { + List findByRole(@Param("role") String role); +} +``` +### /src/main/java/com/in28minutes/springboot/model/Question.java +``` +package com.in28minutes.springboot.model; + +import java.util.List; + +public class Question { + private String id; + private String description; + private String correctAnswer; + private List options; + + // Needed by Caused by: com.fasterxml.jackson.databind.JsonMappingException: + // Can not construct instance of com.in28minutes.springboot.model.Question: + // no suitable constructor found, can not deserialize from Object value + // (missing default constructor or creator, or perhaps need to add/enable + // type information?) + public Question() { + + } + + public Question(String id, String description, String correctAnswer, + List options) { + super(); + this.id = id; + this.description = description; + this.correctAnswer = correctAnswer; + this.options = options; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public String getCorrectAnswer() { + return correctAnswer; + } + + public List getOptions() { + return options; + } + + @Override + public String toString() { + return String + .format("Question [id=%s, description=%s, correctAnswer=%s, options=%s]", + id, description, correctAnswer, options); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (id == null ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Question other = (Question) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + +} +``` +### /src/main/java/com/in28minutes/springboot/model/Survey.java +``` +package com.in28minutes.springboot.model; + +import java.util.List; + +public class Survey { + private String id; + private String title; + private String description; + private List questions; + + public Survey(String id, String title, String description, + List questions) { + super(); + this.id = id; + this.title = title; + this.description = description; + this.questions = questions; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public List getQuestions() { + return questions; + } + + @Override + public String toString() { + return String.format( + "Survey [id=%s, title=%s, description=%s, questions=%s]", id, + title, description, questions); + } + +} +``` +### /src/main/java/com/in28minutes/springboot/service/SurveyService.java +``` +package com.in28minutes.springboot.service; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.model.Survey; + +@Component +public class SurveyService { + private static List surveys = new ArrayList<>(); + static { + Question question1 = new Question("Question1", + "Largest Country in the World", "Russia", Arrays.asList( + "India", "Russia", "United States", "China")); + Question question2 = new Question("Question2", + "Most Populus Country in the World", "China", Arrays.asList( + "India", "Russia", "United States", "China")); + Question question3 = new Question("Question3", + "Highest GDP in the World", "United States", Arrays.asList( + "India", "Russia", "United States", "China")); + Question question4 = new Question("Question4", + "Second largest english speaking country", "India", + Arrays.asList("India", "Russia", "United States", "China")); + + List questions = new ArrayList<>(Arrays.asList(question1, + question2, question3, question4)); + + Survey survey = new Survey("Survey1", "My Favorite Survey", + "Description of the Survey", questions); + + surveys.add(survey); + } + + public List retrieveAllSurveys() { + return surveys; + } + + public Survey retrieveSurvey(String surveyId) { + for (Survey survey : surveys) { + if (survey.getId().equals(surveyId)) { + return survey; + } + } + return null; + } + + public List retrieveQuestions(String surveyId) { + Survey survey = retrieveSurvey(surveyId); + + if (survey == null) { + return null; + } + + return survey.getQuestions(); + } + + public Question retrieveQuestion(String surveyId, String questionId) { + Survey survey = retrieveSurvey(surveyId); + + if (survey == null) { + return null; + } + + for (Question question : survey.getQuestions()) { + if (question.getId().equals(questionId)) { + return question; + } + } + + return null; + } + + private SecureRandom random = new SecureRandom(); + + public Question addQuestion(String surveyId, Question question) { + Survey survey = retrieveSurvey(surveyId); + + if (survey == null) { + return null; + } + + String randomId = new BigInteger(130, random).toString(32); + question.setId(randomId); + + survey.getQuestions().add(question); + + return question; + } +} +``` +### /src/main/java/com/in28minutes/springboot/WelcomeController.java +``` +package com.in28minutes.springboot; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.in28minutes.springboot.configuration.BasicConfiguration; + +@RestController +public class WelcomeController { + + @Autowired + private SomeDependency someDependency; + + @Autowired + private BasicConfiguration configuration; + + @RequestMapping("/") + public String index() { + return someDependency.getSomething(); + } + + @RequestMapping("/dynamic-configuration") + public Map dynamicConfiguration() { + // Not the best practice to use a map to store differnt types! + Map map = new HashMap(); + map.put("message", configuration.getMessage()); + map.put("number", configuration.getNumber()); + map.put("key", configuration.isValue()); + return map; + } + +} + +@Component +class SomeDependency { + + @Value("${welcome.message}") + private String welcomeMessage; + + public String getSomething() { + return welcomeMessage; + } + +} +``` +### /src/main/resources/application.properties +``` +logging.level.org.springframework: INFO +app.name: In28Minutes +app.description: ${app.name} is your first Spring Boot application Properties +welcome.message: Welcome to your first Spring Boot application +basic.value: true +basic.message: Dynamic Message +basic.number: 123 +``` +### /src/main/resources/application.yaml +``` +logging: + level: + org.springframework: DEBUG + +app: + name: In28Minutes + description: ${app.name} is your first Spring Boot application + +welcome: + message: Welcome to your first Spring Boot app! + +basic: + value: true + message: Dynamic Message YAML + number: 100 +``` +### /src/test/java/com/in28minutes/springboot/controller/SurveyControllerIT.java +``` +package com.in28minutes.springboot.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import com.in28minutes.springboot.Application; +import com.in28minutes.springboot.model.Question; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class SurveyControllerIT { + + @LocalServerPort + private int port; + + private TestRestTemplate template = new TestRestTemplate(); + + HttpHeaders headers = new HttpHeaders(); + + @Before + public void setupJSONAcceptType() { + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + } + + @Test + public void retrieveTodo() throws Exception { + + String expected = "{id:Question1,description:Largest Country in the World,correctAnswer:Russia,options:[India,Russia,United States,China]}"; + + ResponseEntity response = template.exchange( + createUrl("/surveys/Survey1/questions/Question1"), + HttpMethod.GET, new HttpEntity("DUMMY_DOESNT_MATTER", + headers), String.class); + + JSONAssert.assertEquals(expected, response.getBody(), false); + } + + @Test + public void retrieveTodos() throws Exception { + ResponseEntity> response = template.exchange( + createUrl("/surveys/Survey1/questions/"), HttpMethod.GET, + new HttpEntity("DUMMY_DOESNT_MATTER", headers), + new ParameterizedTypeReference>() { + }); + + Question sampleQuestion = new Question("Question1", + "Largest Country in the World", "Russia", Arrays.asList( + "India", "Russia", "United States", "China")); + + assertTrue(response.getBody().contains(sampleQuestion)); + } + + @Test + public void addTodo() throws Exception { + Question question = new Question("DOESN'T MATTER", "Smallest Number", + "1", Arrays.asList("1", "2", "3", "4")); + + ResponseEntity response = template.exchange( + createUrl("/surveys/Survey1/questions/"), HttpMethod.POST, + new HttpEntity(question, headers), String.class); + + assertThat(response.getHeaders().get(HttpHeaders.LOCATION).get(0), + containsString("/surveys/Survey1/questions/")); + } + + private String createUrl(String uri) { + return "http://localhost:" + port + uri; + } + +} +``` +### /src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java +``` +package com.in28minutes.springboot.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +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.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.request.MockMvcRequestBuilders; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.service.SurveyService; + +@RunWith(SpringRunner.class) +@WebMvcTest(SurveyController.class) +public class SurveyControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private SurveyService service; + + @Test + public void retrieveQuestion() throws Exception { + + Question mockQuestion = new Question("Question1", "First Alphabet", + "A", Arrays.asList("A", "B", "C", "D")); + + when(service.retrieveQuestion(anyString(), anyString())).thenReturn( + mockQuestion); + + MvcResult result = mvc + .perform( + MockMvcRequestBuilders.get( + "/surveys/Survey1/questions/1").accept( + MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + String expected = "{id:Question1,description:First Alphabet,correctAnswer:A,options:[A,B,C,D]}"; + + JSONAssert.assertEquals(expected, result.getResponse() + .getContentAsString(), false); + + } + + @Test + public void retrieveTodos() throws Exception { + List mockList = Arrays.asList( + new Question("Question1", "First Alphabet", "A", Arrays.asList( + "A", "B", "C", "D")), + new Question("Question2", "Last Alphabet", "Z", Arrays.asList( + "A", "X", "Y", "Z"))); + + when(service.retrieveQuestions(anyString())).thenReturn(mockList); + + MvcResult result = mvc + .perform( + MockMvcRequestBuilders + .get("/surveys/Survey1/questions").accept( + MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + String expected = "[" + + "{id:Question1,description:First Alphabet,correctAnswer:A,options:[A,B,C,D]}," + + "{id:Question2,description:Last Alphabet,correctAnswer:Z,options:[A,X,Y,Z]}" + + "]"; + + JSONAssert.assertEquals(expected, result.getResponse() + .getContentAsString(), false); + } + + @Test + public void createTodo() throws Exception { + Question mockQuestion = new Question("1", "Smallest Number", "1", + Arrays.asList("1", "2", "3", "4")); + + String question = "{\"description\":\"Smallest Number\",\"correctAnswer\":\"1\",\"options\":[\"1\",\"2\",\"3\",\"4\"]}"; + + when(service.addQuestion(anyString(), any(Question.class))).thenReturn( + mockQuestion); + + mvc.perform( + MockMvcRequestBuilders.post("/surveys/Survey1/questions") + .content(question) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect( + header().string("location", + containsString("/surveys/Survey1/questions/1"))); + } +} +``` diff --git a/Step25.zip b/Step25.zip new file mode 100644 index 0000000..4acea75 Binary files /dev/null and b/Step25.zip differ diff --git a/src/main/java/com/in28minutes/springboot/Application.java b/src/main/java/com/in28minutes/springboot/Application.java index f5cdb1d..14326a7 100644 --- a/src/main/java/com/in28minutes/springboot/Application.java +++ b/src/main/java/com/in28minutes/springboot/Application.java @@ -1,18 +1,8 @@ package com.in28minutes.springboot; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import com.in28minutes.springboot.configuration.BasicConfiguration; @SpringBootApplication public class Application { @@ -22,42 +12,4 @@ public static void main(String[] args) { ApplicationContext ctx = SpringApplication.run(Application.class, args); } - @RestController - class SomeBean { - - @Autowired - private SomeDependency someDependency; - - @Autowired - private BasicConfiguration configuration; - - @RequestMapping("/") - public String index() { - return someDependency.getSomething(); - } - - @RequestMapping("/dynamic-configuration") - public Map dynamicConfiguration() { - // Not the best practice to use a map to store differnt types! - Map map = new HashMap(); - map.put("message", configuration.getMessage()); - map.put("number", configuration.getNumber()); - map.put("key", configuration.isValue()); - return map; - } - - } - - @Component - class SomeDependency { - - @Value("${welcome.message}") - private String welcomeMessage; - - public String getSomething() { - return welcomeMessage; - } - - } - } \ No newline at end of file diff --git a/src/main/java/com/in28minutes/springboot/WelcomeController.java b/src/main/java/com/in28minutes/springboot/WelcomeController.java new file mode 100644 index 0000000..5992780 --- /dev/null +++ b/src/main/java/com/in28minutes/springboot/WelcomeController.java @@ -0,0 +1,50 @@ +package com.in28minutes.springboot; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.in28minutes.springboot.configuration.BasicConfiguration; + +@RestController +public class WelcomeController { + + @Autowired + private SomeDependency someDependency; + + @Autowired + private BasicConfiguration configuration; + + @RequestMapping("/") + public String index() { + return someDependency.getSomething(); + } + + @RequestMapping("/dynamic-configuration") + public Map dynamicConfiguration() { + // Not the best practice to use a map to store differnt types! + Map map = new HashMap(); + map.put("message", configuration.getMessage()); + map.put("number", configuration.getNumber()); + map.put("key", configuration.isValue()); + return map; + } + +} + +@Component +class SomeDependency { + + @Value("${welcome.message}") + private String welcomeMessage; + + public String getSomething() { + return welcomeMessage; + } + +} diff --git a/src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java b/src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java new file mode 100644 index 0000000..3a36bd8 --- /dev/null +++ b/src/test/java/com/in28minutes/springboot/controller/SurveyControllerTest.java @@ -0,0 +1,106 @@ +package com.in28minutes.springboot.controller; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +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.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.request.MockMvcRequestBuilders; + +import com.in28minutes.springboot.model.Question; +import com.in28minutes.springboot.service.SurveyService; + +@RunWith(SpringRunner.class) +@WebMvcTest(SurveyController.class) +public class SurveyControllerTest { + + @Autowired + private MockMvc mvc; + + @MockBean + private SurveyService service; + + @Test + public void retrieveQuestion() throws Exception { + + Question mockQuestion = new Question("Question1", "First Alphabet", + "A", Arrays.asList("A", "B", "C", "D")); + + when(service.retrieveQuestion(anyString(), anyString())).thenReturn( + mockQuestion); + + MvcResult result = mvc + .perform( + MockMvcRequestBuilders.get( + "/surveys/Survey1/questions/1").accept( + MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + String expected = "{id:Question1,description:First Alphabet,correctAnswer:A,options:[A,B,C,D]}"; + + JSONAssert.assertEquals(expected, result.getResponse() + .getContentAsString(), false); + + } + + @Test + public void retrieveTodos() throws Exception { + List mockList = Arrays.asList( + new Question("Question1", "First Alphabet", "A", Arrays.asList( + "A", "B", "C", "D")), + new Question("Question2", "Last Alphabet", "Z", Arrays.asList( + "A", "X", "Y", "Z"))); + + when(service.retrieveQuestions(anyString())).thenReturn(mockList); + + MvcResult result = mvc + .perform( + MockMvcRequestBuilders + .get("/surveys/Survey1/questions").accept( + MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn(); + + String expected = "[" + + "{id:Question1,description:First Alphabet,correctAnswer:A,options:[A,B,C,D]}," + + "{id:Question2,description:Last Alphabet,correctAnswer:Z,options:[A,X,Y,Z]}" + + "]"; + + JSONAssert.assertEquals(expected, result.getResponse() + .getContentAsString(), false); + } + + @Test + public void createTodo() throws Exception { + Question mockQuestion = new Question("1", "Smallest Number", "1", + Arrays.asList("1", "2", "3", "4")); + + String question = "{\"description\":\"Smallest Number\",\"correctAnswer\":\"1\",\"options\":[\"1\",\"2\",\"3\",\"4\"]}"; + + when(service.addQuestion(anyString(), any(Question.class))).thenReturn( + mockQuestion); + + mvc.perform( + MockMvcRequestBuilders.post("/surveys/Survey1/questions") + .content(question) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect( + header().string("location", + containsString("/surveys/Survey1/questions/1"))); + } +} \ No newline at end of file