Difference between revisions of "Java: Spring Boot"
Rafahsolis (talk | contribs) Tag: visualeditor |
Rafahsolis (talk | contribs) Tag: visualeditor |
||
| Line 268: | Line 268: | ||
assertThrows(Exception.class, ()-> {functionCallThatThrowsError(params);}) | assertThrows(Exception.class, ()-> {functionCallThatThrowsError(params);}) | ||
| − | assertDoesNotThrows( | + | assertDoesNotThrows( ()-> {functionCallThatDoesNotThrowsError(params);}) |
| − | assertTimeoutPreemptively | + | assertTimeoutPreemptively(Duration.ofSeconds(3), () -> { demoUtils.checkTimeout(); }, "Method should execute in 3 seconds"); |
| + | === Run tests in order === | ||
| + | By default the order of the tests is deterministic but not obious, you can force the order<syntaxhighlight lang="java"> | ||
| + | // MethodOrderer.DisplayName, MethodOrderer..MethodName, MethodOrderer.Random, MethodOrderer.OrderAnnotation | ||
| + | @TestMethodOrder(MethodOrderer.DisplayName.class) | ||
| + | class DemoUtilsTest { | ||
| + | ... | ||
| + | @DisplayName("Equals and not equals") | ||
| + | void testEqualsAndNotEquals()... | ||
| + | |||
| + | // ---------------------------------------------- | ||
| + | |||
| + | @TestMethodOrder(MethodOrderer.OrderAnnotation.class) | ||
| + | class DemoUtilsTest { | ||
| + | ... | ||
| + | @DisplayName("Equals and not equals") | ||
| + | @Order(1) | ||
| + | void testEqualsAndNotEquals()... | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | === Configure maven to run tests === | ||
| + | pom.xml<syntaxhighlight lang="xml"> | ||
| + | <!-- Spring Boot --> | ||
| + | |||
| + | <dependency> | ||
| + | <groupId>org.springframework.boot</groupId> | ||
| + | <artifactId>spring-boot-starter-test</artifactId> | ||
| + | <scope>test</scope> | ||
| + | </dependency> | ||
| + | |||
| + | <!-- Java --> | ||
| + | <dependency> | ||
| + | <groupId>org.apache.maven.plugins</groupId> | ||
| + | <artifactId>maven-surefire-plugin</artifactId> | ||
| + | <version>3.0.0-M5</version> | ||
| + | <scope>test</scope> | ||
| + | </dependency> | ||
| + | |||
| + | </syntaxhighlight> | ||
| + | |||
| + | === Code coverage === | ||
| + | IntelliJ has built-in support for code coverage Maven can too, requires plugin like maven-surefire-report-plugin<syntaxhighlight lang="xml"> | ||
| + | <build> | ||
| + | <plugins> | ||
| + | <plugin> | ||
| + | <groupId>org.apache.maven.plugins</groupId> | ||
| + | <artifactId>maven-surefire-report-plugin</artifactId> | ||
| + | <version>3.0.0-M5</version> | ||
| + | <executions> | ||
| + | <execution> | ||
| + | <phase>test</phase> | ||
| + | <goals> | ||
| + | <goal>report</goal> | ||
| + | <goals> | ||
| + | </execution> | ||
| + | </executions> | ||
| + | |||
| + | </plugin> | ||
| + | </plugins> | ||
| + | </build> | ||
| + | </syntaxhighlight>mvn clean test | ||
| + | |||
| + | mvn site -DgenerateReports=false | ||
| + | <br /><syntaxhighlight lang="xml"> | ||
| + | // at the surefire plugin | ||
| + | |||
| + | .... | ||
| + | <configuration> | ||
| + | <testFailureIgnore>true</testFailureIgnore> | ||
| + | <statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter"> | ||
| + | <usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodname> | ||
| + | </statelessTestsetReporter> | ||
| + | </configuration> | ||
| + | </plugin> | ||
| + | ..... | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | === JaCoCo (Java Code Coverage) === | ||
| + | <syntaxhighlight lang="xml"> | ||
| + | ... | ||
| + | <plugin> | ||
| + | <groupId>org.jacoco</groupId> | ||
| + | <artifactId>jacoco-maven-plugin</artifactId> | ||
| + | <version>0.8.7</version> | ||
| + | |||
| + | <executions> | ||
| + | <execution> | ||
| + | <id>jacoco-prepare</id> | ||
| + | <goals> | ||
| + | <goal>prepare-agent</goal> | ||
| + | </goals> | ||
| + | </execution> | ||
| + | <execution> | ||
| + | <id>jacoco-report</id> | ||
| + | <phase>test</phase> | ||
| + | <goals> | ||
| + | <goal>report</goal> | ||
| + | </goals> | ||
| + | </execution> | ||
| + | </executions> | ||
| + | </plugin> | ||
.... | .... | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | === Disable tests === | ||
| + | <syntaxhighlight lang="java"> | ||
| + | class ConditionalTest { | ||
| + | @Test | ||
| + | @Disabled("Dont run this test until ticket @34 is solved") | ||
| + | void basicTest() {} | ||
| + | |||
| + | @Test | ||
| + | @EnabledOnOs(OS.WINDOWS, OS.MAC, OS.LINUX) | ||
| + | void testForAllSystems() {} | ||
| + | |||
| + | @Test | ||
| + | @EnabledOnJre(JRE.JAVA_17) | ||
| + | void testOnJavaVersion() {} | ||
| + | |||
| + | @Test | ||
| + | @EnabledOnJreRange(min=JRE.JAVA_13, max=JRE.JAVA_18) // If only min provided works until latest java | ||
| + | void testOnJavaVersionRange() {} | ||
| + | |||
| + | @Test | ||
| + | @EnabledIfSystemProperty(named="SOMEPROPERTYNAME", matches="SOMEVALUE") | ||
| + | void testOnJavaVersion() {} | ||
| + | |||
| + | @Test | ||
| + | @EnabledIfEnvironmentVariable(named="SOMEPROPERTYNAME", matches="SOMEVALUE") | ||
| + | void testOnJavaVersion() {} | ||
| + | |||
| + | |||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | === Parameterized Tests === | ||
| + | <syntaxhighlight lang="java"> | ||
| + | // @parameterizedTest | ||
| + | @parameterizedTest(name="value={0}, expected={1}") | ||
| + | /* @CsvSource({ | ||
| + | "1,1", | ||
| + | "2,2", | ||
| + | "3,Fizz", | ||
| + | "4,4", | ||
| + | "5,Buzz" | ||
| + | })*/ | ||
| + | @CsvFileSource(resources="/small-test-data.csv") // src/test/resources/small-test-data.csv | ||
| + | void testCsvData(int value, String expected){ | ||
| + | assertEquals(expected, FizzBuzz.compute(value)); | ||
| + | } | ||
| + | </syntaxhighlight> | ||
==Example: Spring Boot with Data JPA and in memory database H2== | ==Example: Spring Boot with Data JPA and in memory database H2== | ||
Revision as of 13:38, 23 June 2022
Create microservices
import java.util.Arrays;
import java.util.List;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
public class Course {
private long id;
private String name;
private String author;
public Course(long id, String name, String author){
super();
this.id = id;
this.name = name;
this.author = author;
}
public String toString() {
return "Course [id=" + id + ", name= " + name + ", author=" + author + " ]";
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public String getAuthor() {
return author;
}
}
@RestController
public class CourseController {
@GetMapping("/courses")
public List<Course> getAllCourses(){
return Arrays.asList(new Course(1, "Learn microservices", "in28minutes"));
}
}
Spring Boot Starter Projects
Found at Spring Initializr Under dependencies
Web Application: Spring Boot Starter Web
REST API: Spring Boot Starter Web
Talk to database using JPA: Spring Boot Starter Data JPA
Talk to database using JDBC: Spring Boot Starter Data JDBC
Secure your web application or REST API: Spring Boot Starter Security
Spring Boot Actuator
Monitor and manage your application
- beans: Complete list of Spring beans in your app
- health: Application health information
- metrics: Application metrics
- mappings: Details around Requests Mappings
To enable Spring Boot actuator at pom.xml:
....
<dependencies>
....
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
....
</dependencies>
....
http://127.0.0.1:8080/actuator
http://127.0.0.1:8080/actuator/health
http://127.0.0.1:8080/actuator/health/{*path}
http://127.0.0.1:8080/actuator/info
You can enable more endpoints of actuator at application.properties:
# anagement.endpoints.web.exposure.include=*
# anagement.endpoints.web.exposure.include=health,metrics
Dev Tools
Add at pom.xml
....
<dependencies>
....
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-devtools</artifactId>
<scope>runtime</scope>
</dependency>
....
</dependencies>
....
<scope>runtime</scope> --> runs only at development
Provides:
- Develop server reloading on changes (pom.xml changes are not detected)
Unit tests
Add Maven dependencies for JUnit
....
<dependencies>
....
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
....
</dependencies>
....
Package structure to create tests
The convention is tu follow the structure shown on the image.
Test example
package com.luv2code.junitdemo;
import org.junit.jupiter.api.Test;
// import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import static org.junit.jupiter.api.Assertions.*;
// https://leeturner.me/posts/building-a-camel-case-junit5-displaynamegenerator/
// @DisplayNameGeneration(DisplayNameGenerator.Simple.class)
// @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class)
class DemoUtilsTest {
DemoUtils demoUtils;
@BeforeAll
static void setUpClass() {
System.out.println("Cleanup before all tests")
}
@BeforeEach
void setUp{
demoUtils = new DemoUtils();
int expected = 6;
}
@Test
// @DisplayName("Null and Not Null")
void testEqualsAndNotEquals() {
// Execute
int actual = demoUtils.add(2, 4);
// Assert
assertEquals(expected, actual, "2 + 4 must be 6");
}
@Test
void testNullAndNotNull(){
String str1 = null;
String str2 = "luv2code";
assertNull(demoUtils.checkNull(str1), "Object should be null");
assertNull(demoUtils.checkNull(str2), "Object should not be null");
}
@AfterEach
void tearDownAfterEach(){
System.out.println("Cleanup After each test");
}
@AfterAll
static void cleanUpClass(){
System.out.println("Cleanup After all tests");
}
}
Custom displayName generator for camelCase with numbers
static class ReplaceCamelCaseEmojis extends ReplaceCamelCase {
public ReplaceCamelCaseEmojis() {
}
public String generateDisplayNameForClass(Class<?> testClass) {
return this.replaceWithEmojis(super.generateDisplayNameForClass(testClass));
}
public String generateDisplayNameForNestedClass(Class<?> nestedClass) {
return this.replaceWithEmojis(super.generateDisplayNameForNestedClass(nestedClass));
}
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
return this.replaceWithEmojis(super.generateDisplayNameForMethod(testClass, testMethod));
}
private String replaceWithEmojis(String name) {
name = name.replaceAll("Camel|camel", "\uD83D\uDC2B");
name = name.replaceAll("Case|case", "\uD83D\uDCBC");
name = name.replaceAll("Display|display", "\uD83D\uDCBB");
name = name.replaceAll("Divisible|divisible", "\u2797");
name = name.replaceAll("Year|year", "\uD83D\uDCC5");
name = name.replaceAll("100", "\uD83D\uDCAF");
return name;
}
}
Run unit test
All tests
mvn test
Single test
mvn -Dtest=TestMessageBuilder test
Single test method from one test class
mvn -Dtest=TestMessageBuilder#testHelloWorld test
JUnit Assertions
import static org.junit.jupiter.api.Assertions.*;
org.junit.jupiter.api.Assertions contains JUnit Assertions
assertEquals(expected, actual, optional_message)
assertNotEquals(unexpected, actual, optional_message)
assertNull()
assertNotNull()
assertSame(object1, object1, message)
assertNotSame(object1, object1, message)
assertTrue(boolvariable, message)
assertFalse(boolvariable, message)
assertArraysEqual(array1, array2, message)
assertIterableEquals(iterable1, iterable2 message)
assertLinesMatch??
assertThrows(Exception.class, ()-> {functionCallThatThrowsError(params);})
assertDoesNotThrows( ()-> {functionCallThatDoesNotThrowsError(params);})
assertTimeoutPreemptively(Duration.ofSeconds(3), () -> { demoUtils.checkTimeout(); }, "Method should execute in 3 seconds");
Run tests in order
By default the order of the tests is deterministic but not obious, you can force the order
// MethodOrderer.DisplayName, MethodOrderer..MethodName, MethodOrderer.Random, MethodOrderer.OrderAnnotation
@TestMethodOrder(MethodOrderer.DisplayName.class)
class DemoUtilsTest {
...
@DisplayName("Equals and not equals")
void testEqualsAndNotEquals()...
// ----------------------------------------------
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class DemoUtilsTest {
...
@DisplayName("Equals and not equals")
@Order(1)
void testEqualsAndNotEquals()...
Configure maven to run tests
pom.xml
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Java -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<scope>test</scope>
</dependency>
Code coverage
IntelliJ has built-in support for code coverage Maven can too, requires plugin like maven-surefire-report-plugin
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.0.0-M5</version>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>report</goal>
<goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
mvn clean test
mvn site -DgenerateReports=false
// at the surefire plugin
....
<configuration>
<testFailureIgnore>true</testFailureIgnore>
<statelessTestsetReporter implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
<usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodname>
</statelessTestsetReporter>
</configuration>
</plugin>
.....
JaCoCo (Java Code Coverage)
...
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<id>jacoco-prepare</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
....
Disable tests
class ConditionalTest {
@Test
@Disabled("Dont run this test until ticket @34 is solved")
void basicTest() {}
@Test
@EnabledOnOs(OS.WINDOWS, OS.MAC, OS.LINUX)
void testForAllSystems() {}
@Test
@EnabledOnJre(JRE.JAVA_17)
void testOnJavaVersion() {}
@Test
@EnabledOnJreRange(min=JRE.JAVA_13, max=JRE.JAVA_18) // If only min provided works until latest java
void testOnJavaVersionRange() {}
@Test
@EnabledIfSystemProperty(named="SOMEPROPERTYNAME", matches="SOMEVALUE")
void testOnJavaVersion() {}
@Test
@EnabledIfEnvironmentVariable(named="SOMEPROPERTYNAME", matches="SOMEVALUE")
void testOnJavaVersion() {}
}
Parameterized Tests
// @parameterizedTest
@parameterizedTest(name="value={0}, expected={1}")
/* @CsvSource({
"1,1",
"2,2",
"3,Fizz",
"4,4",
"5,Buzz"
})*/
@CsvFileSource(resources="/small-test-data.csv") // src/test/resources/small-test-data.csv
void testCsvData(int value, String expected){
assertEquals(expected, FizzBuzz.compute(value));
}
Example: Spring Boot with Data JPA and in memory database H2
/pom.xml
....
<dependencies>
....
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-devtools</artifactId>
<scope>runtime</scope>
</dependency>
....
</dependencies>
....
To run against a MySQL instead of H2 replace the h2 dependency by:
....
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
....
/src/main/resources/application.properties
h2 Database
spring.datasource.url=jdbc:h2:mem:testdb
# If using Spring Boot >=2.5.0 the following line is also required
spring.jpa.defer-datasource-initialization=true
database will be accessible via http://localhost:8080/h2-console
Docker MySQL
#spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/courses
spring.datasource.username=courses-user
spring.datasource.password=dummycourses
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
#courses-user@localhost:3306
Start MySQL Docker container
docker run --detach --env MYSQL_ROOT_PASSWORD=dummypassword --env MYSQL_USER=courses-user --env MYSQL_PASSWORD=dummycourses --env MYSQL_DATABASE=courses --name mysql --publish 3306:3306 mysql:5.7
mysqlsh commands
mysqlsh
\connect courses-user@localhost:3306
\sql
use courses
select * from course;
\quit
# Stop MySQL container
docker container ls
docker container stop ID
/src/main/java/com/in28minutes/learnspringboot/courses/bean/Course.java
package com.in28minutes.learnspringboot.courses.bean;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
// import javax.persistence.Column;
// @Entity(name="db_table_name")
@Entity
public class Course {
@Id
@GeneratedValue
private long id;
// @Column(name="db_column_name")
private String name;
private String author;
public Course(){} // Entities must have a default constructor
public Course(long id, String name, String author){
super();
this.id = id;
this.name = name;
this.author = author;
}
public String toString() {
return "Course [id=" + id + ", name= " + name + ", author=" + author + " ]";
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public String getAuthor() {
return author;
}
}
src/main/resources/data.sql
querys in this file will be executed at startup automatically
insert into COURSE(ID, AUTHOR, NAME)
values(100001, 'in28minutes', 'Learn Microservices');
insert into COURSE(ID, AUTHOR, NAME)
values(100002, 'in28minutes', 'Learn FullStack with React and Angular');
insert into COURSE(ID, AUTHOR, NAME)
values(100003, 'in28minutes', 'Learn AWS, GCP and Azure');
/src/main/java/com/in28minutes/learnspringboot/courses/repository/CourseRepository.java
package com.in28minutes.learnspringboot.courses.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.in28minutes.learnspringboot.courses.bean.Course;
public interface CourseRepository extends JpaRepository<Course, Long> {}
/src/main/java/com/in28minutes/learnspringboot/courses/controller/CourseController.java
package com.in28minutes.learnspringboot.courses.controller;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
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;
import com.in28minutes.learnspringboot.courses.bean.Course;
import com.in28minutes.learnspringboot.courses.repository.CourseRepository;
@RestController
public class CourseController {
@Autowired
private CourseRepository repository;
// http://localhost:8080/courses
@GetMapping("/courses")
public List<Course> getAllCourses() {
return repository.findAll();
}
//// http://localhost:8080/courses/1
@GetMapping("/courses/{id}")
public Course getCourseDetails(@PathVariable long id) {
Optional<Course> course = repository.findById(id);
if(course.isEmpty()) {
throw new RuntimeException("Course not found with id " + id);
}
return course.get();
}
/*
POST http://localhost:8080/courses
{
"name": "Learn DevOps",
"author": "in28minutes"
}*/
//POST - Create a new resource (/courses)
@PostMapping("/courses")
public void createCourse(@RequestBody Course course){
repository.save(course);
}
/*
PUT - http://localhost:8080/courses/100001
{
"id": 100001,
"name": "Learn Microservices 2",
"author": "in28minutes"
}
*/
//PUT - Update/Replace a resource (/courses/1)
@PutMapping("/courses/{id}")
public void updateCourse(@PathVariable long id, @RequestBody Course course){
repository.save(course);
}
//DELETE - Delete a resource (/courses/1)
@DeleteMapping("/courses/{id}")
public void deleteCourse(@PathVariable long id){
repository.deleteById(id);
}
// docker run --detach
// --env MYSQL_ROOT_PASSWORD=dummypassword
// --env MYSQL_USER=courses-user
// --env MYSQL_PASSWORD=dummycourses
// --env MYSQL_DATABASE=courses
// --name mysql
// --publish 3306:3306 mysql:5.7
}
REST API
GET
Retrieve information
/courses, /courses/1
POST
Create a new resource
/courses
PUT
Update/Replace a resource
/courses/1
PATCH
Update a part of the resource
/courses/1
DELETE
Delete a resource
/courses/1
Build
Make JAR not WAR (JAR contains embeded server)