NTM Solutions

Thứ Tư, 26 tháng 4, 2023

Giới thiệu tool Swagger - Màn hình màu cho JUnit Tests

[ Link bài viết gốc ]

Mến chào các bạn lần đầu đến với chủ đề Java Spring trên lopHocViTinh.com!

Hôm nay chúng ta sẽ demo 01 Restful API Backend có gắn thêm đồ chơi Swagger - công cụ tương tự Junit Tests nhưng có màn hình dạng web cực kỳ trực quan - dễ sử dụng.

Đầu tiên chúng ta tạo mới 01 project Spring Boot bằng cách vào trang này:

https://start.spring.io/

p/s: các bạn cũng có thể tạo project trong IDE của mình đang có (Ví dụ: IntelliJ Idea hoặc VS Code)


Ở đây mình chọn Gradle để có gì chạy offline -> các thư viện vẫn chạy ok.

Sau đây là toàn bộ cài đặt trong file build.gradle

plugins {
id 'java'
id 'org.springframework.boot' version '2.7.7'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
mavenCentral()
}

dependencies {
// https://mvnrepository.com/artifact/io.springfox/springfox-swagger2
implementation group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
// https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui
implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'

// https://mvnrepository.com/artifact/io.springfox/springfox-boot-starter
//implementation group: 'io.springfox', name: 'springfox-boot-starter', version: '3.0.0'

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
useJUnitPlatform()
}


Các bạn nào tạo project trên web spring.io theo mình nên né đầu giờ sáng ra (có lẽ vì giờ này lập trình viên vào nhiều chăng???)

Chúng ta tạo 01 cơ sở dữ liệu tên: db_demo 

(ở đây mình dùng MySQL WorkBench - tool mặc định của Oracle)

create database `db_demo`;
use `db_demo`;

create table `todo`(
`id` int not null auto_increment,
name varchar(128),
status int,
primary key(id)
)ENGINE=InnoDB;


INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('1', 'Hotdog', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('2', 'Jelly roll', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('3', 'Croissant', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('4', 'Hamburger bun', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('5', 'Bagels', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('6', 'Donut', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('7', 'Rolls', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('8', 'Breadsticks', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('9', 'Baguette', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('10', 'Tiramisu', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('11', 'Biscuit', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('12', 'Pizza', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('13', 'Cheese cake', 1);
INSERT INTO `db_demo`.`todo` (`id`, `name`, `status`) VALUES ('14', 'Pate chaud', 1);

//truncate table `todo`;
//drop table `todo`;
//drop database `db_demo`;

Đừng chạy 03 dòng cuối cùng nhé!!!

Tiếp theo là file cấu hình kết nối cơ sở dữ liệu: application.properties

server.port=8080
spring.jpa.hibernate.ddl-auto=update
#install mysql using Docker
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_demo
spring.datasource.username=root
spring.datasource.password=*****
#allow table's name like tblProduct
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.properties.hibernate.format_sql=true

spring.mvc.pathmatch.matching-strategy=ant-path-matcher
Các bạn chú ý dùm cho dòng cấu hình cuối cùng -> Đây chính là dòng fix lỗi swagger (mình đã mò chỗ này rất lâu mới tìm được)

Trong thư mục project backendapi tạo thư mục config chứa file cấu hình: SwaggerConfig.java

package backendapi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
}

Tạo tiếp thư mục controller chứa 02 file sau:

TodoApiController.java

package backendapi.controller;

import backendapi.model.Todo;
import backendapi.service.ITodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/todo")
@CrossOrigin //phải có cái này lúc truy xuất bằng JS mới cho phép
public class TodoApiController {
@Autowired
ITodoService todoService;

@GetMapping()
public ResponseEntity<Iterable<Todo>> findAllTodo() {
List<Todo> todos = (List<Todo>) todoService.findAll();
if(todos.isEmpty()){
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
return new ResponseEntity<>(todos, HttpStatus.OK);
}

@PostMapping
public ResponseEntity<Todo> saveTodo(@RequestBody Todo todo) {
return new ResponseEntity<>(todoService.save(todo), HttpStatus.CREATED);
}

@PutMapping("/{id}")
public ResponseEntity<Todo> updateTodo(@PathVariable Long id, @RequestBody Todo todo) {
Optional<Todo> todoOptional = todoService.findById(id);
if(!todoOptional.isPresent()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
todo.setId(todoOptional.get().getId());
return new ResponseEntity<>(todoService.save(todo), HttpStatus.OK);
}

@DeleteMapping("/{id}")
public ResponseEntity<Todo> deleteTodo(@PathVariable Long id) {
Optional<Todo> todoOptional = todoService.findById(id);
if(!todoOptional.isPresent()){
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
todoService.remove(id);
return new ResponseEntity<>(todoOptional.get(), HttpStatus.NO_CONTENT);
}
}

CachingController.java (bài này mình tạo thêm tính năng Cache và Clear Cache để load dữ liệu từ API cho nhanh)

package backendapi.controller;

import backendapi.service.CachingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CachingController {

@Autowired
CachingService cachingService;

@GetMapping("clearAllCaches")
public void clearAllCaches() {
cachingService.evictAllCaches();
}
}

Thư mục tạo tiếp theo là model chứa file: Todo.java

package backendapi.model;

import javax.persistence.*;
import java.io.Serializable;

@Entity
@Table(name="todo")
public class Todo implements Serializable {
private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name="name")
private String name;

@Column(name="status")
private int status;

public Todo() {}

public Todo(String name, int status) {
this.name = name;
this.status = status;
}

@Override
public String toString() {
return "Todo{" +
"id=" + id +
", name='" + name + '\'' +
", status=" + status +
'}';
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}
}

Tạo thư mục repository chứa file: ITodoRepository.java

package backendapi.repository;

import backendapi.model.Todo;
import org.springframework.data.repository.PagingAndSortingRepository;

public interface ITodoRepository extends PagingAndSortingRepository<Todo, Long> {
}

Cuối cùng là thư mục service chứa các file như sau:

CachingService.java

package backendapi.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;

@Service
public class CachingService {
@Autowired
CacheManager cacheManager;
public void evictAllCaches() {
System.out.println(cacheManager.getCacheNames());

cacheManager.getCacheNames().stream()
.forEach(cacheName -> cacheManager.getCache(cacheName).clear());

System.out.println(cacheManager.getCacheNames());
}
}

ITodoService.java

package backendapi.service;

import backendapi.model.Todo;

public interface ITodoService extends IGeneralService<Todo, Long>{
}

TodoService.java

package backendapi.service;

import backendapi.model.Todo;
import backendapi.repository.ITodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class TodoService implements ITodoService {
@Autowired
private ITodoRepository todoRepository;

@Override
@Cacheable("todos")
public Iterable<Todo> findAll(){return todoRepository.findAll(); }

@Override
public Optional<Todo> findById(Long id){ return todoRepository.findById(id); }

@Override
public Todo save(Todo todo){ return todoRepository.save(todo); }

@Override
public void remove(Long id){ todoRepository.deleteById(id);}
}


File chạy của chương trình: BackendapiApplication.java

package backendapi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
//@EnableSwagger2
public class BackendapiApplication {

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

}
/*
link test Swagger2:
http://localhost:8080/swagger-ui.html
*/

Bạn nào vẫn thích xài JUnit Tests thì...của bạn đây:
package backendapi;

import backendapi.controller.CategoryApiController;
import backendapi.service.CategoryService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
public class NullTests {
@Autowired
private CategoryApiController categoryApiController;

@Autowired
private CategoryService categoryService;

@Autowired
private MockMvc mockMvc;

@Test
void contextLoads(){//đảm bảo có dữ liệu trong 02 khu vực quan trọng-> kết nối success
assertThat(categoryApiController).isNotNull();
assertThat(categoryService).isNotNull();
}

@Test
public void shouldReturnStatusOK() throws Exception{//test status 200
this.mockMvc.perform(get("/api/category")).andDo(print()).andExpect(status().isOk());
}
}

p/s: ví dụ này mình dùng cho controller category -> sửa lại thành TodoApiController dùm mình.


Cuối cùng, chúng ta chạy chương trình BackendApiApplication -> Mở trình duyệt, vào liên kết sau để xem thành quả: 
http://localhost:8080/swagger-ui.html

Bấm Try it out

Các tính năng chính của chương trình:

Màn hình file Readme

[ Link bài viết gốc ]
Tác giả #drM

Không có nhận xét nào:

Đăng nhận xét

Facebook Youtube RSS