Controllers
Learn
A controller is the first place on HTTP request goes once it reaches your application. it is, in essence, translator between the HTTP request and your code. In spring Boot, we use the @RestController
annotation to tell spring that our class is controller.
Controller contain endpoints, which describe the URIs the client application can access. we use annotation for these as well, which we will discuss later on.
What is JUnit?​
Junit is a common java unit testing framework. It comes bundled with Intellij and makes automating test cse simple.
What is REST Assured?​
Rest Assured is a HTTP functional testing framework. It works by generating real HTTP request to an endpoint and allowing you to run assertions against the responses.
note
Any code below with a package or import that includes bootcamp
as part of its path should replace bootcamp
with your project name.
Do​
1. Create New Working Branch​
It is good practice when making changes to a git codebase to create nd work on new branch so that your changes do not impact production code should they contain an error. A new branch should be made off your primary branch for every new feature/modification.
First, try to get the latest code to avoid merge conflicts and resolve if any.
git pull
Create a new local branch and switch to it executive the following command int the Intellij terminal:
git checkout -b <branch>
where <branch>
is the name of new branch.
The checkout
command will move us to this new branch while the -b
flag will create this branch for us should it not yet exist. We are now working on a new branch.
2. Write Student
object​
Write the Student
POJO (plain old Java object) with a default constructor, parameterized constructor, and getters.
In the src/main/java/com.geteche.students
directory, create a new package called model
.
In the newly created package, create a new java class called Student.java
.
Include private variables id
and name
, use Intellij to generate getters for those fields, and add 2 constructors in the class.
- Code Snippet
- Full Code
public class Student {
private Integer id;
private String name;
public Student() {}
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
...
}
If copying the full file you must replace the bootcamp
path in the package name and import to match your project name.
package com.geteche.students.model;
public class Student{
private Integer id;
private String name;
public Student() {}
public Student(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
note
For those who are more familiar with java, the reason why we use the Integer
class instead of the int
primitive here will become clear later on.
3. Write StudentController
​
Write the StudentController
with one GET
endpoint that will retrieve a single Student
.
In the src/main/java/com.geteche.students
directory, create a new package called controller
.
In the newly created package, create a new Java class called StudentController.java
.
Based on the annotation and method header in the code snippet below, implement the getStudentById
method to return single Student
object.
- Code Snippet
- Full Code
@GetMapping(
value = "/v1/students/{studentId}",
products = MediaType.APPLICATION_JSON_VALUE)
public Student getStudentById(@PathVariable("studentId") Integer id)
If copying the full file you must replace the bootcamp
path in the package name imports to match your project name.
package com.geteche.students.controller;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.geteche.students.model.Student;
@RestController
public class StudentController {
@GetMapping(value = "v1/students/{studentId}", produces = MediaType.APPLICATION_JSON_VALUE)
public Student getStudentById(@PathVariable("studentId") Integer id) {
return new Student(id, "John");
}
}
We use the @GetMapping
annotation to tell the controller that we want requests to the given HTTP GET
endpoint to call the handler methods we describe. we call this a mapping.
There are two parameters in this annotation:
value
, which tells the controller what the URI should look like; andproduces
, which tells the controller that the HTTP response it sends back to the client should be encoded as JSON (JavaScript Object Notation).
The String we pass to value
is taced on to the end of the URI (hence, endpoint). Notice the braces ({}
) in the string. These braces denote a variable in the URI. Each of our Student
objects will be given a unique ID, and if we use this ID in our HTTP request, the controller will parse it and assign its value to a parameter in the method call. we do this with the @PathVariable
annotation you can see in the method header.
For example, if you wanted to retrieve the Student with ID 1, your request URL would look like: http://localhost:8080/api/v1/students/1
. The application would then use the value 1
in its (hidden) call to getStudentById()
.
4. Configure base URL​
- Configuration
server.servlet.context-path=/api
5. Write tests​
Write a JUnit unit test and a REST Assured integration test for the StudentController
.
Unit test​
In the src/test/java/com.geteche.students
directory, create a new package called controller
.
In the newly created package, create a new Java class called StudentControllerTest.java
.
Use the @SpringBootTest
annotation to tell Spring Boot to start a Spring application context for the @SpringBootApplication
, include a dependency on StudentController
, and use the @Test
annotation to signify the method is a unit test.
- Code Snippet
- Full Code
@SpringBootTest
public class StudentControllerTest {
@Autowired
private StudentController StudentController;
@Test
public void getStudentByIdTest() {
...
}
...
}
If copying the full file you mush replace the bootcamp
path in the package name and imports to match your project name.
package com.geteche.students.controller;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.geteche.students.model.Student;
@SpringBootTest
public class StudentControllerTest {
@Autowired
private StudentController StudentController;
@Test
public void getStudentByIdTest() {
Integer id = 1;
Student Student = StudentController.getStudentById(id);
assertThat(Student.getId()).isEqualTo(id);
assertThat(Student.getName()).isEqualTo("Butterfingers");
}
}
To execute the unit tests, run the following command in the Intellij terminal:
- Windows
- MacOs
.\gradlew test
./gradlew test
6. Debug​
It is important to know how to debug, especially when trying to fix your tests that might be broken. In unit tests, it is as easy as setting breakpoints and start debugging. But in integration tests, you need to create a remote JVM debug configuration and attach debugger to process, because your application is running in Docker container, which is seen as a remote application.
7. Commit and push changes​
Now that your changes have been made to the codebase, it is time to stage all of your changes for committing by executing:
git add .
note
Using .
will add of your changed files to the commit. Sometimes, you may not want to add everything. In this case, you can instead provide the names of specific files or directories you wish to commit using:
git add <file>
Alternatively, you can review all of the changes you made before adding them with the command:
git add --patch
Now, commit your changes by executing:
git commit -m "<message>"
where <message>
is your commit message describing your changes.
tip
It's common practice to write commit messages in the present tense rather than in the past tense (e.g. "Fix broken link" instead of "Fixed broken link"). Additionally, keep your commit messages as concise as possible. This blog post by Chris Beams provides an in-depth discussion on the importance of good commit messages, as well as guidelines for writing good messages.
Finally, push your changes to a new remote branch in your Github repository by executing:
git push --set-upstream origin <branch>
where <branch>
is your new branch name from before.
note
Instead of --set-upstream
, you can also use the shorthand -u
flag. It will achieve the same result. And this is usually needed the first time only. After that, you don't need this flag.
8. Open pull request​
In GitHub, open a pull request and select one or more individuals to review your changes. Provide additional comments as necessary.
As soon as you create the pull request, a new build in Jenkins will start. This build will build and test your new code and ensure that is passes quality gateways. If the build fails, you may need to go back and make changes to your code.
9. Merge branch into master
​
After your code has been reviewed and the Jenkins build has completed successfully, you will be able to merge your code into master
branch. After doing so, keeping in line with the trunk-based branching strategy, you can safely delete the branch.
After merging into master
another Jenkins build will start to deploy your application to nonprod.
10. Prepare for further development​
Now that the changes have been merged with the remote master
branch, we need to update our local copy. To do so, first switch over to your local copy of master
by executing the following command in the IntelliJ terminal:
git checkout master
Next, retrieve your changes by executing:
git pull
You can also safely delete your old branch by executing:
git branch -d <branch>
where <branch>
is the name of your branch from before.
note
Sometimes, Git will warn your that there are uncommitted changes on the branch your are truing to delete or that your local branch has not been fully merged. If, however, you know that all the correct changes have been committed, pushed, and merged successfully, you can use the -D
flag (instead of -d
) to force the branch to be deleted.