Difference between revisions of "Java: Spring Boot"

From RHS Wiki
Jump to navigation Jump to search
Tag: visualeditor
Tag: visualeditor
Line 268: Line 268:
 
assertThrows(Exception.class, ()-> {functionCallThatThrowsError(params);})
 
assertThrows(Exception.class, ()-> {functionCallThatThrowsError(params);})
  
assertDoesNotThrows(Exception.class, ()-> {functionCallThatDoesNotThrowsError(params);})
+
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)