Saturday, September 25, 2021

Deploying Spring Boot Application With Docker and Minikube on Windows

 image

Prerequisites:

  1. Spring Boot Application
  2. Docker should be installed.
  3. Minikube should be installed.

1. Spring Boot Application:

Here we have simple application with one controller. Nothing much to discuss.

2. Create Dockerfile in below path.

image

FROM adoptopenjdk/openjdk11:alpine-jre
ADD target/SpringBoot-Docker-Kubernates-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]

Before build Dockerfile check whether minikube is started? type command minikube status you will get the status as below, image

Here it is running. If not you can run the command minikube start.

Normally docker images will not be available for minikube. So run minikube docker-env. This will give below command. image

Run @FOR /f "tokens=*" %i IN ('minikube -p minikube docker-env') DO @%i in command prompt. Now if you run docker images we can get images for minikube. So

Now to build this application type following command. docker build -t springboot-k8s:2.0 . Now our application image is available in the docker images as below,

image

3. Create deployment and service yaml file for Kubernetes

  • Create Deployment yaml file:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-k8s-deploy
spec:
  selector:
    matchLabels:
      app: springboot-k8s
  replicas: 3
  template:
    metadata:
      labels:
        app: springboot-k8s
    spec:
      containers:
        - name: springboot-k8s
          image: springboot-k8s:2.0
          ports:
            - containerPort: 6060
  • Create Service yaml file:
apiVersion: v1
kind: Service
metadata:
  name: springboot-k8s-service
  labels:
    name: springboot-k8s
spec:
  ports:
    - nodePort: 31875
      port: 6060
      targetPort: 6060
      protocol: TCP
  selector:
    app: springboot-k8s
  type: NodePort
  • Run Both Service and Deployment file one by one with below command:
kubectl apply -f deployment.yml
kubectl apply -f service.yml
  • Now if you run below command:
kubectl get all

image

We can see all pods, services and deployments here.

  • Run below command to get the url of minikube to access the services.
minikube service springboot-k8s-service --url

Now we can get the url to access minikube as per below screenshot. image

image

That's all about deployment of spring boot application with Docker and kubernetes (minikube). We'll catch up with other topics in future.


Key Notes:

To access Ubuntu terminal in windows, 

1. we should type kubectl config view in command prompt. Then copy the server ip. 

2. Then we should go to terminal and type vi ~/.kube/config. This will open config file. There we should edit the below highlighted part from the screenshot. Mostly it will be port number.










Friday, July 3, 2020

Test Driven Development With Spring Boot, JUnit 5 And Mockito


image

1. About TDD:

1.1 So, what is it?

image
  • What TDD actually is, is a cycle.
  • You write a very simple test that fails. Then you write as little code as possible to make the test pass. You then write a slightly more complex test case that fails. Then you make it pass with as little code as possible. And around and around you go, in this cycle that should be complete in mere minutes (if not seconds).
  • This cycle is known as the Red-> Green cycle.

1.2 You must refactor!

image
  • However, there is an extremely important step between the passes and the next failure. You must refactor your code where appropriate. You could make any test pass with enough if statements and hard-coding, but it would be useless code. Useful code is better. And when you refactor, you refactor without fear of breaking your existing functionality. You have no fear, because your full set of tests will let you know if anything breaks.
  • This cycle is known as the Red-> Green-> Refactor cycle. This cycle is Test Driven Development.

1.3 So why should we do it?

  • You will be fearless
  • Code will be streamlined
  • You will reduce debugging time.
  • Your tests will become the most comprehensive set of documentation imaginable
  • Your code will have better design
  • Need not worry about code coverage

1.4 Drawbacks

  • Bugs in Tests
  • Slower at the beginning
  • All the members of the team need to do it
  • Test need to be maintained when requirements changes

2. Let's start with an Example:

Mostly now a days we follow agile workflow model. So we will get requirements via UserStories. Here let's assume that we got one userstory with following endpoints.
Acceptance Criteria:
Expose below Rest URLS:
1./cars/{name} - Get Car details by name [GET]
2./cars/{name} - Throw CarNotFoundException

2.1 Create Spring Boot Application with any IDE you prefer.

I'm using Intelij here where Spring Assistant plugin is used. image
Select Spring Boot version and required Libraries. image
Since we are going to use Junit 5 along with Spring boot Please include below dependencies too,
<dependency>
 <groupId>org.junit.jupiter</groupId>
 <artifactId>junit-jupiter-api</artifactId>
 <scope>test</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency>
 <groupId>org.junit.jupiter</groupId>
 <artifactId>junit-jupiter-engine</artifactId>
 <scope>test</scope>
</dependency>

2.2 Let's start with Controller Unit Test

Let's start from Controller. I'm going to take you to the tour where how step by step programming is happening with the help of Test Driven Development Approach. First we'll create CarControllerTest. Here we are going to create an end point for - /cars/{name}.
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = CarController.class)
public class CarControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void getCar_Details() throws Exception{
        mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
                .andExpect(status().isOk());
    }

}
At line @WebMvcTest(controllers = CarController.class) code will give you an error saying that CarController class is not available. Hence we'll go to src/main folder and will create just CarController without Body.
public class CarController {

}
Now the compilation error is resolved. When we run CarControllerTest class now, we end-up with the failed message as below,
image
The reason for below error is that, because there is no rest endpoint with url /cars/Scala in CarController class. Let's create the endpoint in CarController class.
@RestController
@RequestMapping("/cars")
public class CarController {

   @GetMapping("/{name}")
    public ResponseEntity<Car> getCarDetails(@PathVariable String name) throws Exception {
        return new ResponseEntity<>( HttpStatus.OK);
    }
}
Once again we'll run the CarControllerTest class. image Yes it is passed now. Hurray!! guys we created endpoint successfully.
Now are focus is to return the Car details, for which we need Car model. Let's create Car model class under model package.
@Entity
@Table(name="CARS")
public class Car {

    @Id
    @GeneratedValue
    private Long id;

    @Column
    private String name;

    @Column
    private String type;

    public Car() {}

    public Car(String name, String type) {
        this.name = name;
        this.type = type;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
}
Now in CarController class lets add below codes,
@RestController
@RequestMapping("/cars")
public class CarController {

   @GetMapping("/{name}")
    public ResponseEntity<Car> getCarDetails(@PathVariable String name) throws Exception {
        Car car = new Car();
        return new ResponseEntity<>( car,HttpStatus.OK);
    }
}
Now lets navigate to CarControllerTest class and add few more point to existing table as below,
@Test
    public void getCar_Details() throws Exception{

        mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isMap())
                .andExpect(jsonPath("name").value("Scala"))
                .andExpect(jsonPath("type").value("Sadan"));
    }
From above code, what are we expecting from test is that, when we pass /cars/Scala, then the response should contain Car object with Scala as name and Sadan as type. Now we'll execute the test. image The reason for this that we are passing car object with null values in it.
Lets assume that from external class CarService, we will get the car details. Base on that assumption, in CarController lets change below line,
@Autowired
CarService carService;
    
@GetMapping("/{name}")
public ResponseEntity<Car> getCarDetails(@PathVariable String name) throws Exception {
 Car car = carService.getCarDetails(name);
 return new ResponseEntity<>( car,HttpStatus.OK);
}
Now we need to create CarService class with method getCarDetails without body.
@Service
public class CarService {

    public Car getCarDetails(String name) {
       return null;
    }
}

2.3 Introduction of Mockito

As I said earlier Since we are going to focus only on controller we will mock any class which is external to CarController class.
Now in CarControllerTest, we are going to mock CarService class and give the definition for getCarDetails method present in it.
    @MockBean
    CarService carService;

    @Test
    public void getCar_Details() throws Exception{
        given(carService.getCarDetails(Mockito.anyString())).willReturn(new Car("Scala","Sadan"));

        mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$").isMap())
                .andExpect(jsonPath("name").value("Scala"))
                .andExpect(jsonPath("type").value("Sadan"));
    }
Here we are mocking CarService object with @MockBean annotation.
given(carService.getCarDetails(Mockito.anyString())).willReturn(new Car("Scala","Sadan"));
With above line we are defining the behaviour in such a way that, if we pass any String as name it should return new Car details. Now lets run the test once again. Yes it is passed.
Lets assume if no car details avaliable for the given name, what would happen. For this scenario we need to create CarNotFoundException class which will be throwed when no car details present for given name.
@ResponseStatus(code= HttpStatus.NOT_FOUND)
public class CarNotFoundException extends RuntimeException {

    public CarNotFoundException() {}
}
Again from CarControllerTest we are going to have another test to validate this scenario.
    @Test
    public void Car_NotFoud_HttpStatus() throws Exception{
        given(carService.getCarDetails(Mockito.anyString())).willThrow(new CarNotFoundException());

        mockMvc.perform(MockMvcRequestBuilders.get("/cars/Scala"))
                .andExpect(status().isNotFound());
    }
If you run this test method it will work like charm. So far we completed two scenarios one with valid response and another with Exception throwed in Controller class level.
Now lets move to CarService Class. As we know so far we did not touch CarService class as part of CarControllerTest. Now Lets create CarServiceTest which is dedicated to CarService class.

2.4 Service Unit Test

Here we are going to use only Mockito related setup to ensure that how getCarDetails method is working. Here also we have two scenarios one with valid result from CarRepository and another with CarNotFoundException. Create CarRepository with findByName(name) interface first.
public interface CarRepository {
    public Optional<Car> findByName(String name);
}
Now create CarServiceTest class,
public class CarServiceTest {

    @Mock
    CarRepository carRepository;

    @InjectMocks
    CarService carService;

    @BeforeEach
    public void setUp(){
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void getCarDetails() throws Exception{
        given(carRepository.findByName("pulse")).willReturn(Optional.of(new Car("pulse", "hatchback")));

        Car car = carService.getCarDetails("pulse");
        assertNotNull(car);
        assertEquals("pulse",car.getName());
        assertEquals("hatchback",car.getType());

    }

    @Test
    public void getCar_NotFound_Test(){
        given(carRepository.findByName("pulse")).willThrow(new CarNotFoundException());

        assertThrows(CarNotFoundException.class, ()-> carService.getCarDetails("pulse"));
    }

}
As per above code we can see we did not bother about the logic behind CarRepository class. We are just mocking them by our expectations. we can run now. Yes both test methods are passed.

2.5 Repository Unit Test

Now lets focus on CarRepository interface. We need to ensure that the CarRepository's method findByName should give us proper data fetched from database. Here we are going to use Embedded H2Database. Under src/main/resources folder add data.sql file just like below,
DROP TABLE IF EXISTS CARS;

CREATE TABLE CARS (
  id INT AUTO_INCREMENT  PRIMARY KEY,
  name VARCHAR(250) NOT NULL,
  type VARCHAR(250) NOT NULL
);

INSERT INTO CARS (id, name, type) VALUES ('1001','duster','hybrid');
INSERT INTO CARS (id, name, type) VALUES ('1002','micra','hatchback');
INSERT INTO CARS (id, name, type) VALUES ('1003','lodgy','suv');
What will happen here is that when we run @DataJpaTest annotated CarRepositoryTest class, these data will be stored in H2 Database untill the execution of test method is over.
Create CarRepositoryTest Class,
@ExtendWith(SpringExtension.class)
@DataJpaTest
class CarRepositoryTest {

    @Autowired
    private CarRepository carRepository;

    @Test
    public void testFindByName() {
        Optional<Car> car = carRepository.findByName("duster");
        assertTrue(car.isPresent());
    }

    @Test
    public void testFindByName_Not_Found(){
        Optional<Car> car = carRepository.findByName("pulse");
        assertFalse(car.isPresent());
    }
}
Lets run this, image Yes these cases passed. If you see the highlighted part, the query is executed to fetch the data from database.

2.6 Cache Test

Now lets focus on Cache test in our application. Lets go and add @EnableCaching to Application class,
@SpringBootApplication
@EnableCaching
public class Application {

 public static void main(String[] args) {
  SpringApplication.run(Application.class, args);
 }

}
Now go to CarService class and annotate getCarDetails method with @Cacheable("cars").
Lets create CacheTest class. Just be clear that since we are going to verify cache, we need to use @SpringBootTest in this class,
@SpringBootTest
public class CacheTest {

    @MockBean
    CarRepository carRepository;

    @Autowired
    CarService carService;

    @Test
    void cacheTest() {
        given(carRepository.findByName("pulse")).willReturn(Optional.of(new Car("pulse", "hatchback")));

        Car car = carService.getCarDetails("pulse");
        assertNotNull(car);
        carService.getCarDetails("pulse");

        Mockito.verify(carRepository,Mockito.times(1)).findByName("pulse");

    }
}
Here with the help of Mockito's verify method we are ensuring that carRepository's findByName method is called only once,though we called carService.getCarDetails() twice.

2.7 Integration Test

Now let create IntegrationTest class to ensure entire flow is working fine.
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IntegrationTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    CarService carService;

    HttpHeaders headers = new HttpHeaders();

    @Test
    public void getCarDetails() throws Exception {
        HttpEntity<String> entity = new HttpEntity<String>(null,headers);
        ResponseEntity<Car> response = restTemplate.exchange(
                "http://localhost:"+port+"/cars/duster", HttpMethod.GET, entity, Car.class);
        assertEquals(HttpStatus.OK,response.getStatusCode());
        assertEquals("hybrid",response.getBody().getType());
    }
}
If we run all Test at once we may get following out put from Intelij, image
So finally we came to end of the session. That all about Test Driven Development approach towards creation of Spring Boot application.

Download from GitLab