Gom3rye

JPA Test 본문

현대 오토에버 클라우드 스쿨

JPA Test

Gom3rye 2025. 8. 22. 17:44
728x90
반응형
=>JPA Test
- 개요
스프링 프레임워크에서 제공하는 @Repository 어노테이션은 데이터베이스에 데이터를 생성, 삭제, 수정, 조회하는 기능을 제공하는 클래스를 스프링 빈으로 정의할 때 사용
 
@WebMvcTest 어노테이션이 WebMvc 영역을 구분해서 빠르게 테스트 하는 것 처럼 데이터를 처리하는 영역을 구분해서 빠르게 테스트하는 방법이 @DataJpaTest 나 @DataMongoTest
 
@DataJpaTest 어노테이션은 @Repository만 스캔하므로 @Service, @Component, @Controller 어노테이션은 스캔하지 않음
 
@DataJpaTest를 사용하지 않고 @SpringBootTest 어노테이션을 사용하여 테스트 케이스를 작성한다면 테스트 케이스마다 @Transactional 어노테이션을 정의하는 것이 바람직한데 @DataJpaTest는 소스 내부에 @Transactional 어노테이션을 포함하고 있어 별도로 정의하지 않아도 되는데 테스트 케이스에 @Transactional 어노테이션을 추가하면 테스트 종료 후 자동으로 롤백되므로 다시 초기화 상태가 되지만 SpringBootTest의 environment 속성을 WebEnvironment.RANDOM_PORT 나 DEFINED_PORT로 설정하면 롤백되지 않는데 이 설정 때문에 테스트 케이스를 실행하면 별도의 서블릿 컨테이너가 생성되서 실행
 
테스트 케이스를 실행하는 스레드와 테스트 케이스가 호출한 서블릿 컨테이너의 스레드가 서로 달라서 서블릿 컨테이너의 트랜잭션을 테스트 케이스에 롤백할 수 없기 때문입니다.
 
현업에서 테스트 케이스를 유지보수하는 일은 매우 어렵기 때문에 테스트 케이스를 애플리케이션의 일부라고 생각하지 않은 사람이 많으면 시간에 쫒기면 가장 먼저 포기하는 것이 테스트 케이스인데 테스트 케이스는 지속 가능한 애플리케이션을 만들 수 있는 가장 중요한 자산 중 하나
 
버그가 발생하거나 기능을 추가할 때 마다 테스트 케이스를 작성하게 되면 애플리케이션을 서비스한 기간 만큼 테스트 케이스가 많아질 것이고 많아진 테스트 케이스는 애플리케이션의 대부분의 기능을 테스트 할 것이고 버그에 대응한 테스트가 많아지면 별도의 노력없이 자동으로 회귀 테스트가 수행됨
 
테스트 케이스가 많아지면 언제든 리팩토링 할 수 있고 동일한 에러가 다시 발생하지 않도록 하는 신뢰성 있는 서비스가 될 것입니다.
 
Controller를 테스트 할 때 REST-API를 직접 호출해서 기능을 테스트하는 것 보다는 테스트 케이스를 사용하여 개발한 기능을 테스트하는 습관을 갖는 것이 바람직
 
7)Unit Test(단위 테스트)
=>개요
- 단위 테스트는 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 동작하는지 검증하는 절차
- 모든 함수와 메서드에 대한 테스트 케이스를 작성하는 절차로 이를 통해서 언제라도 코드 변경으로 인한 문제가 발생할 경우 단 시간 내에 이를 파악하고 바로 잡을 수 있도록 합니다. 
- 각 테스트 케이스는 서로 분리되어야 하고 이를 위해서 Mock Object를 생성하는 것도 좋은 방법
- 유닛 테스트는 개발자 뿐 만 아니라 보다 심도있는 테스트를 위해서 테스터에 의해서 수행하기도 함
 
=>장점
- 문제점 발견
유닛 테스트의 목적은 프로그램의 각 부분을 고립시켜서 각각의 부분이 정확하게 동작하는지 확인하는 것으로 프로그램을 작은 단위로 쪼개서 각 단위가 정확하게 동작하는지 검사하고 이를 통해서 문제 발생 시 정확하게 어느 부분이 잘못되었는지를 빨리 확인할 수 있기 때문에 프로그램의 안정성이 높아짐
 
유닛 테스트는 개발 시간을 증가시키는 것 처럼 보이지만 개발 기간 중 대부분을 차지하는 디버깅 시간을 줄여줌
 
- 변경이 쉬움
리팩토링 후에도 해당 모듈이 의도대로 작동하고 있음을 유닛 테스트를 통해서 확신할 수 있는데 이를 회귀 테스트라고 하며 어떻게 코드를 고치더라도 문제점을 금방 파악할 수 있고 수정된 코드가 정확하게 동작하는지 쉽게 할 수 있으므로 코드 변경을 자주 할 수 있게 됩니다.
 
- 통합이 간단
유닛 테스트는 유닛 자체의 불확실성을 제거해주므로 상향식 테스트 방식에서 유용
먼저 프로그램의 각 부분을 검증하고 그 부분들을 합쳐서 다시 검증하는 통합 테스트에서 더욱 유용
 
=>Spring Boot 단위 테스트
- mysql 설치: docker run -d --name mysql-container -e MYSQL_ROOT_PASSWORD=비밀번호 -p 3306:3306 mysql:8.0
 
- mysql 과 spring-web, jpa, lombok 의존성을 가진 프로젝트 생성
 
- application.properties 파일을 삭제하고 application.yaml 파일을 만들어서 작성
server:
  port: 9090
 
spring:
  config:
    activate:
      on-profile: default
 
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mysql?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
    username: root
    password: wnddkd
  
  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: create
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    show-sql: true
    properties:
      hibernate.hibernate.format_sql: true
      dialect: org.hibernate.dialect.MySQL8InnoDBDialect
      
logging:
  level:
    org.hibernate.SQL: debug 
 
- 운영 환경의 데이터베이스에 테스트 코드로 접근하게 되면 운영 환경에 데이터가 변경이 되므로 테스트 환경은 별도의 데이터베이스를 사용하는 것이 좋고 이 때 메모리 데이터베이스를 사용하게 되면 운영 환경에 영향을 주지 않고 테스트 가능 
 
- h2라는 메모리 데이터베이스를 사용하기 위해서 build.gradle에 의존성을 추가
testImplementation 'com.h2database:h2'
 
- resource 디렉토리에 설정 파일을 추가하고 작성: application-test.yml
spring:
  config:
    activate:
      on-profile: test
  datasource:
    url: jdbc:h2:mem:test
    driver-class-name: org.h2.Driver
    username: sa
    password:
 
- Entity 클래스 추가: Member
 
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
 
@Entity
@Getter
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(nullable = false)
    private String name;
 
    @Column(nullable = false, unique = true)
    private String email;
 
    @Column(nullable = false)
    private int age;
 
    @Builder
    public Member(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
 
}
 
- Repository 생성: MemberRepository
import com.example.demo.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
 
public interface MemberRepository extends JpaRepository<Member,Long> {
}
 
- Sevice 클래스 생성: MemberService
import com.example.demo.entity.Member;
import com.example.demo.persistence.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
 
@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
    
    public void save(){
        memberRepository.save(Member.builder().age(1).email("email@email.com").name("name").build());
    }
}
 
- Controller 클래스 생성: MemberController
import com.example.demo.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;
 
    @RequestMapping("/")
    String save(){
        memberService.save();
        return "Success";
    }
    
    @RequestMapping("/health")
    String health(){
        return "OK";
    }
}
 
- test 디렉토리에 Repository 테스트용 클래스를 추가
import com.example.demo.entity.Member;
import com.example.demo.persistence.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
 
@ActiveProfiles("test")
//이 어노테이션이 추가되면 모든 작업은 롤백되빈다.
@DataJpaTest
@TestPropertySource(locations = "classpath:application-test.yml")
public class MemberRepositoryTestDatabase {
    @Autowired
    MemberRepository memberRepository;
 
    @Test
    void save(){
        memberRepository.save(Member.builder().age(1).email("email@email.com").name("name").build());
    }
 
}
 
- test 디렉토리에 Controller 테스트 용 클래스 추가
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
 
import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class ControllerTest {
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    public void testController() throws Exception{
        mockMvc.perform(post("/health").contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(MockMvcResultHandlers.print(System.out));
    }
}
 
- gradle에서 테스트 명령어
./gradlew compileJava
./gradlew test
 
- jenkins 에서 스크립트를 이용해서 테스트
stage("permission"){
steps{
sh "chmod +x ./gradlew"
}
}
stage("compile"){
steps{
sh "./gradlew compileJava"
}
}
stage("test"){
steps{
sh "./gradlew test"
}
}
 
 
4.Code Coverage
1)개요
=>코드 커버리지는 소프트웨어 테스트에서 사용되는 지표 중 하나로 코드의 실행 여부를 기반으로 코드의 품질 과 완성도를 측정하는 방법
=>테스트 케이스를 실행할 때 코드 내의 각 요소가 실행되는 빈도를 측정해서 테스트 케이스가 커버하는 코드의 비율을 나타내는 지표
=>커드 커버리지는 테스트 케이스의 실행 여부를 기준으로 계산하는데 이를 통해서 테스트 케이스가 커버하지 않은 코드 블록을 실별해서 개발자가 이를 보완할 수 있도록 도와줌
=>코드 커버리지의 일반적인 측정 요소
Statement Coverage: 실행된 문장의 수 / 전체 문장 수
Branch Coverage: 실행되는 조건문 블록 수 / 전체 조건문 블록 수
Function Coverage: 실햄 된 함수 수 / 전체 함수 수
Decision Coverage: 수행된 분기 수 / 전체 분기 수
Condition Coverage: 수행된 조건 수 / 전체 조건 수
 
2)JaCoCo Code Coverage
=>설정 및 수행
- build.gradle 파일에 plugins에 플러그 인 추가
 id 'jacoco'
 
- build.gradle 파일에 설정 추가
jacocoTestCoverageVerification{
    violationRules {
        rule{
            limit {minimum = 0.2} //전체 테스트 커버리지가 20%이상이어야 빌드를 진행
        }
    }
}
 
- 실행
./gradlew test jacocoTestCoverageVerification
 
./gradlew test jacocoTestReport
 
- 보고서 확인
http://localhost:63342/프로젝트이름/build/reports/jacoco/test/html/index.html
 
프로젝트 안의 build/reports/jacoco 디렉토리 안의 index.html을 브라우저에서 열어도 됩니다.
 
=>결과 해석
- Class Coverage
클래스가 1번 이상 사용되었는지를 기준으로 커버리지를 계산
클래스 사용 기준은 클래스 내 메서드 중 1개 이상 사용, 생성자, static initializer 호출 시 클래스가 사용된 것으로 판단
 
- Method Coverage
메서드가 1번 이상 호출되었는지를 기준으로 커버리지를 계산
메서드 내부의 모든 코드가 실행되었는지는 판단 기준에서 제외
실행된 메서드의 개수 / 전체 메서드의 개수 * 100
 
- Line Coverage
테스트 중에 실행된 소스 코드 라인 수 와 전체 소스 코드 라인 수 중 몇 개의 라인이 실행되었는지를 측정
테스트에서 놓친 코드 라인이 얼마나 있는지 확인할 수 있음
 
- Branch Coverage
if 나 switch에 대해 테스트의 실행 흐름이 어떻게 되는지 측정하는 것
특정한 조건에 따라 코드 실행 흐름이 달라질 때 모든 분기문이 정상적으로 실행되는지 확인 할 수 있음
Brance Coverage가 100%라면 Line Coverage도 100%
Line Coverage가 100%라고 해서 Branch Coverage가 100%는 아님
 
- Instructions Coverage
Jacoco에서 제공하는 가장 작은 단위의 커버리지 측정 방법
자바 바이트 코드 단위에서 실행되었거나 실행되지 않는 코드의 양에 대한 정보를 측정
바이트 코드를 확인하고자 할 때는 jclasslib 같은 플러그인을 통해서 확인이 가능
 
- Complexity by Type
JaCoCo 리포트에서는 cxty로 표현되는데 코드의 복잡도를 나타내는 매트릭
클래스나 인터페이스 와 같은 코드 유형의 복잡도를 측정하는데 사용
순환 복잡도 계산 공식
B(the number of branches) - D(the number of decision points) + 1
 
if 나 for, while 등은 1씩 증가
switch - case의 경우는 case 수 만큼 증가
삼항 연산자나 &&, || 의 경우도 1증가 시킴
catch의 경우도 1증가
 
위의 경우를 전부 계산한 다음 1을 추가해서 순환복잡도를 계산
 
3)Code Coverage의 이점
=>버그 감소: 테스트 코드가 더 만은 부분을 실행하므로 버그를 사전에 검출하는데 도움이 되는데 코드 커버리지가 낮으면 테스트 되지 않는 코드에서 버그가 발생할 수 있음
=>코드 품질 향상: 더 많은 코드가 테스트되고 버그가 감소하게 되므로 코드의 안정성 과 신뢰성이 높아짐
=>유지보수 용이성: 코드 변경 사항에 대한 테스트 코드를 작성하는 것이 더 쉬워짐
=>개발 생산성 향상
 
4)Code Coverage의 목표
=>코드 커버리지가 100%라고 하더라도 무결점 소프트웨어를 의미하지는 않음
=>코드 커버리지가 100%라고 하더라도 문제점 발생
- 요구 사항에 있는 기능이 누락된 경우
- 구현에 오류가 있는 경우
- 테스트 케이스가 누락된 경우
=>단순히 코드 커버리지를 높이기 위해서 테스트 케이스를 생성하는 일은 지양해야하며 요구 사항 및 구현 코드에 대한 이해를 바탕으로 올바른 테스트 케이스를 작성해야 함
=>Google 의 Testing Blog에 작성된 Code Coverage Best Practice 글을 보면 코드 커버리지는 코드의 비지니스 영향/중요도 또는 코드를 얼마나 자주 건드려야 하는지, 코드의 복잡성, 유지 기간 및 도메인 변수에 따라 도메인 오너가 수준을 정하는 것이 좋다고 가이드 하고 있고 60%를 적절한 수준 75%면 칭찬받을 만한 수준 그리고 90%를 모범적인 수준으로 가이드
=>메소드 커버리지는 100%를 기본으로 하고 Instruction Coverage에 대한 지표를 도입하고 Branch Coverage를 더 향상시킬 수 있는 도전적인 목표를 수립하고 차근 차근 진행해 나가면 코드의 품질이 좋아질 것 
 
5.SonarQube
1)정적 코드 분석
=>개요
- 소스 코드를 실행하지 않고 코드의 품질을 검사하는 과정
- 코드를 읽기 쉽고 유지보수하기 좋도록 만드는 것을 보증
 
=> 목적
- 코드 품질 개선: 복잡한 코드, 중복 코드, 나쁜 코드 스타일을 검출
- 버그 사전 탐지: 런타임 오류를 초래할 수 있는 잠재적 문제 탐지
- 보안 강화: SQL Injection, XSS, 취약한 암호화 알고리즘 과 같은 보안 취약점 검출
- 코드 일관성 유지: 정해진 코딩 표준 및 규칙 준수 여부 확인
 
=>도구
- SonarQube
- Checkstyle
- SpotBugs
- PMD
- FindSecBugs
 
2)SonarQube
=>CI + Analysis
- Code Quality Assurance Tool -> Issues + Defect + Code Complexity
- Detect Bugs & Vulnerabilities
- Track Code Smells
- Code Quality Metrics & History
=>17가지 정도의 다양한 언어 지원: Java, C#, go, python, javascript, CloudFormation, Terraform 등
=>CI/CD Integration
=>다양한 플러그인이 제공
 
=>설치
- 바이너리를 다운로드 받아서 설치 가능
- 도커로 설치
docker run --rm -p 9000:9000 --name sonarqube sonarqube
계정은 admin 비밀번호도 admin
 
=>Spring Boot Application에 Sonarqube 사용
- Token 생성
소나큐브에 접속해서 자신의 계정 메뉴를 눌러서 MyAccount > Security 메뉴에서 토큰을 생성
 
squ_9e646551088b32be57737b01af671c121999330f
 
=>build.gradle 파일에 플러그인 설치
id("org.sonarqube") version "6.0.1.5171"
 
=>build.gradle 파일에 소나큐브 설정을 추가
sonar {
    properties {
        property "sonar.host.url", "http://localhost:9000"
        property "sonar.login", "squ_9e646551088b32be57737b01af671c121999330f"
        property "sonar.projectKey", "demo3"
        property "sonar.projectName", "spring-boot app"
    }
}
 
=>검사
./gradlew clean build sonar
 
=>Controller 클래스의 코드 수정
import com.example.demo.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequiredArgsConstructor
public class MemberController {
 
    private final MemberService memberService;
 
    private final Logger logger = LoggerFactory.getLogger(MemberController.class);
    @RequestMapping("/")
    String save(){
        memberService.save();
        return "Success";
    }
 
    @RequestMapping("/health")
    String health(){
        System.out.println("출력하는 코드");
        return "OK";
    }
}
 
=>검사 
 
3)Jenkins에서 위의 모든 작업을 수행
=>소스 코드를 git hub에 업로드
 
=>Jenkins 설치
- 도커로 설치
docker run -d --name jenkins -p 8080:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock jenkins/jenkins:lts
 
- docker logs jenkins 를 수행해서 초기 비밀번호를 확인해서 로그인하고 비밀번호 설정 후 기본 플러그 인 설치
- 로그가 보이지 않으면 docker exec -it jenkins /bin/bash -> /var/jenkins_home/secrets/ 으로 이동 후 cat initialAdminPassword
 
- 테스트 코드 수행
pipeline{
    agent any
    
    stages{
        stage("git"){
            steps{
                git branch:'master', url:'https://github.com/itggangpae/test.git'
            }
        }
        stage("Permission"){
            steps{
                sh "chmod +x ./gradlew"
            }
        }
        stage("compile"){
            steps{
                sh "./gradlew compileJava"
            }
        }
        stage("test"){
            steps{
                sh "./gradlew test"
            }
        }
        
    }
    
}
 
- JaCoCo Code Coverage 수행
 
pipeline{
    agent any
    
    stages{
        stage("git"){
            steps{
                git branch:'master', url:'https://github.com/itggangpae/test.git'
            }
        }
        stage("Permission"){
            steps{
                sh "chmod +x ./gradlew"
            }
        }
        stage("compile"){
            steps{
                sh "./gradlew compileJava"
            }
        }
        stage("test"){
            steps{
                sh "./gradlew test"
            }
        }
        
        stage("jacoco"){
            steps{
                sh './gradlew test jacocoTestCoverageVerification'
            }
        }
        
    }
    
}
 
- sonarqube 수행
728x90
반응형

'현대 오토에버 클라우드 스쿨' 카테고리의 다른 글

데이터 센터 이상치 알림 프로젝트  (0) 2025.09.04
Argo CD  (3) 2025.08.22
TDD  (0) 2025.08.20
프로메테우스  (0) 2025.08.19
프로메테우스  (6) 2025.08.18