Junit5 Test Container사용하여 테스트 환경 구축하기 (인프런 백기선님 강의 정리)

web/Junit|2019. 12. 26. 15:21

도커와 테스트 (TestContainers)

테스트를 위해서는 운영과 동일한 형태의 개발 환경에서 테스트 하는 것이 중요하다. 하지만 매번 동일하게 환경을 구축할 수 없고 모든 개발 자들과 같은 환경을 맞추기도 쉽지 않다.

그래서 Docker를 이용해서 테스트마다 테스트를 위한 컨테이너를 실행시켜서 테스트하고 컨테이너를 제거해주면 좋은데 그런 기능을 TestContainer를 이용해서 가능하다. 실제로 이번에 이직한 회사에서 동일하게 테스트 환경을 사용하는 것을 봤다. 막연하게 그 기능을 사용할 수 있었지만 이번 Junit5 백기선님 강의를 들어서 확실하게 정리할 수 있어서 좋았다. 역시 듣길 잘했다. 꼭 들어보길 강추한다. 그럼 그 내용을 정리해보자.

테스트 컨테이너(Test Container) 라이브러리 추가 및 설정

https://www.testcontainers.org를 보고 테스트에 필요한 모듈의 라이브러리를 추가하면 된다. 나는 gradle을 사용하여 mysql을 사용해야 하기에 다음과 같이 추가했다.

testCompile "org.testcontainers:testcontainers:1.12.4"
testCompile "org.testcontainers:junit-jupiter:1.12.4"
testCompile "org.testcontainers:mysql:1.12.4"

그리고 생성된 mysql test container에서 사용할 설정 값들에 대한 설정이 필요한대 기존 설정과는 다르게 다음과 같이 별도의 jdbc url을 적어줘야한다. 이도 testcontainer 홈페이지에 모듈 설명에 나와있다.

spring:
  datasource:
    url: jdbc:tc:mysql:5.7.22://localhost:3306/test
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver

    dbcp2:
      driver-class-name: com.mysql.jdbc.Driver
      test-on-borrow: true
      validation-query: SELECT 1
      max-total: 1

  jpa:
    show-sql: true

테스트 수행

라이브러리와 연결에 대한 설정을 모두 완료하였으면 테스트를 진행해야한다. 테스트 컨테이너를 이용해서 테스트를 진행하기 위해서는 클래스에 @TestContainers 어노테이션을 붙이고 불러들인 MysqlContainer에 어노테이션 @Container를 붙여주면 된다. 그리고 container가 테스트가 실행될 때 시작하고 종료되면 같이 끝나기 위해서 @BeforeAll과 @AfeterAll을 지정해준다.

package com.wedul.javajunit5studyjunit.docker;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import javax.transaction.Transactional;
import static org.assertj.core.api.Assertions.*;
/**
 * java-junit5-study
 *
 * @author wedul
 * @since 2019/12/24
 **/
@ActiveProfiles("test")
@SpringBootTest
@Testcontainers
class StudentServiceTest {

    @Autowired
    StudentService studentService;

    @Container
    static MySQLContainer mariaDBContainer = new MySQLContainer();

    @Test
    @DisplayName("학생 추가하기")
    @Transactional
    void create_student_test() {
        studentService.createStudent(Student.builder()
            .studentId(2L)
            .age(10)
            .name("wedul")
            .address("seoul jamsil")
            .build()
        );
    }

    @DisplayName("student 조회 테스트")
    @Test
    @Transactional
    void find_student_test() {
        studentService.createStudent(Student.builder()
            .studentId(2L)
            .age(10)
            .name("wedul")
            .address("seoul jamsil")
            .studentNickname("duri")
            .build()
        );
        Student student = studentService.getStudent(2L);
        assertThat(student).isNotNull();
    }

}

만약 별도록 MysqlContainer처럼 지정이 되어 있지 않은 컨테이너를 올려서 테스트 하고 싶을 때는 GenericContainers를 사용하여 공식 이미지를 다운받아서 사용할수도 있다. 자세한건 강의를 참조하면 좋다.

@Container
static GenericContainer genericContainer = new GenericContainer("mysql");

Docker container 값을 스프링 value로 사용하기

서비스로 올라간 Docker container에 속성을 spring에서 사용하고 싶을때는 Application의 설정값을 읽어서 사용할수 있도록 해주는 ApplicationContextInitializer를 구현하여 값을 application에 전달하여 사용할 수 있다.

우선 ApplicaionContextInitializer를 구현하여 컨테이너 속성 값을 environment에 넘겨주고 이를 스프링 value로 꺼내서 사용하면 된다.

@ActiveProfiles("test")
@SpringBootTest
@Testcontainers
@ContextConfiguration(initializers = StudentServiceTest.ContainerPropertyInitializer.class)
class StudentServiceTest {

    @Autowired
    StudentService studentService;

    @Container
    static MySQLContainer mySQLContainer = new MySQLContainer();

    @Value("${container.databaseName}")
    private String databaseName;

    @Test
    @DisplayName("학생 추가하기")
    @Transactional
    void create_student_test() {
        System.out.println(databaseName);
        studentService.createStudent(Student.builder()
            .studentId(2L)
            .age(10)
            .name("wedul")
            .address("seoul jamsil")
            .build()
        );
    }

    @DisplayName("student 조회 테스트")
    @Test
    @Transactional
    void find_student_test() {
        studentService.createStudent(Student.builder()
            .studentId(2L)
            .age(10)
            .name("wedul")
            .address("seoul jamsil")
            .studentNickname("duri")
            .build()
        );
        Student student = studentService.getStudent(1L);
        assertThat(student).isNotNull();
    }

    static class ContainerPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            TestPropertyValues.of("container.databaseName=" + mySQLContainer.getDatabaseName())
                .applyTo(applicationContext.getEnvironment());
        }
    }

}

Docker Compose를 사용하여 테스트하기

매번 새로운 설정을 프로그램상이나 yml으로 정의하는건 너무 번거롭다. 그래서 생성할 컨테이너들을 한번에 기재해서 컨테이너로 올릴 때 사용하는 docker compose를 사용하면 편리하다. 간단하게 mysql 컨테이너를 올릴 yml을 만들고 테스트에서 사용해보자. docker-compose 파일을 읽어들일 때는 DockerComposeContainer를 사용해서 올리면 된다.

### docker-compose.yml 파일

version: '3.1'

services:
  maria:
    image: mariadb:latest
    restart: "always"
    ports:
    - "13306:3306"
    environment:
      - MYSQL_ROOT_PASSWORD=test
      - MYSQL_DATABASE=test
      - MYSQL_USER=wedul_dev
      - MYSQL_PASSWORD=test
// 사용방법
@Container
static DockerComposeContainer composeContainer =
    new DockerComposeContainer(new File("src/test/resources/docker-compose.yml"));

그리고 위에서는 host에서 연결할 port 13306을 지정하였으나 실제로는 지정하지 않고 하면 랜덤포트를 자유롭게 이용하기 때문에 더 좋을 것 같다. 왜냐하면 해보니까 내가 지정한 포트가 다른 개발자 컴퓨터에서는 이미 다른 컨테이너로써 운영중일 수도 있기 때문에 랜덤 포트를 사용하게 하는게 좋은 것 같다.

ports:
- "3306"

 

근데 사용해보니 단점이 있다. container_name과 같이 몇개의 docker-compose에서 제공하는 몇개의 부가 속성들을 제대로 읽어들이지 못하고 오류를 뱉어내기도 한다. 다음과 같이 에러가 발생되어서 지웠다 ㅜㅜ

docker-compose.yml has 'container_name' property set for service 'maria' but this property is not supported by Testcontainers, consider removing it

docker-compose를 매번 테스트할때마다 레포지토리에 dev-tool에 놔뒀다가 사용했었는데 확실히 편해지고 좋을 것 같다. 잘 활용해보자.

출처 : https://www.inflearn.com/course/the-java-application-test
github : https://github.com/weduls/junit5

댓글()

Spring BootJunit5 테스트 (백기선님 인프런 강의)

web/Junit|2019. 12. 23. 20:34

Junit 5 테스트

Junit4를 잘 알고 있던건 아니지만 새로 입사한 회사에서 Junit5를 사용하여 테스트 코드를 짜기때문에 더 잘 알고 싶어 공부하게 되었다. 그 중 백기선님의 Junit5 테스트 코드 관련 인강을 인프런에서 듣게 되었다. 내용이 너무 좋았고 그동안 몰랐고 정리가 되지 않았던 부분을 많이 알게 되었다. 이를 아주 간략하게만 정리해봤다. 가격이 그리 비싸지 않기 때문에 한번쯤은 꼭 보는걸 추천한다.
https://www.inflearn.com/course/the-java-application-test/#


소개

  • Junit5는 Junit3, 4에서 사용하던 Junit Platform 구현체 Vintage대신 Jupiter를 사용해서 TestEngine Api를 사용하는 test 프레임워크이다.
  • Spring Boot 2.2.x가 릴리즈된 이후로는 공식적으로 Spring-boot-starter-tester에 제공되고 있다.
  • Junit4까지는 test 코드가 public 하여야 했지만 Junit5 부터는 클래스, 메소드 모두 public하지 않아도 된다.


추가된 기능

Test 이름 지정
테스트의 이름을 지정할 때 기존에 _(언더스코어)를 사용하여 많이 작명하였었다.

@Test
public void when_join_not_error() {
}

하지만 이와 같은 방식으로 사용하게 되면 테스트가 실행되었을 때 내용을 보는데 많이 불편하다.

이런 불편함을 junit5의 Display 전략을 이용하면 편하게 볼 수 있다.

첫 번째 방법으로 @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)을 사용하여 _(언더스코이)이름을 언더스코어를 제거한 이름으로 만들어 줄 수 있다.

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class WedulTest {

    @Test
    void when_join_not_error() {

    }

}

하지만 단순하게 _(언더스코어)만 바꿔줬다고 가시적으로 보이지는 않는다. 그래서 테스트 이름을 직접 지정해 줄 수 있다.

@Test
@DisplayName("가입시 에러가 발생하는지 테스트")
void when_join_not_error() {

}


Assert 방법
기본적으로 Junit5에서 제공하는 Assert는 아래와 같은 방식으로 사용할 수 있다.

image

  • expected : 기대하는 결과물
  • actual : 실제 값
  • message : 기대하는 결과물과 실제 값이 달랐을 때 발생될 문자

예를 들어 Wedul이라는 객체에 id를 가져왔을 때 1L인지 테스트하고 아니면 "아이디가 다릅니다." 메시지가 호출하게 해보자.

@Test
@DisplayName("가입시 에러가 발생하는지 테스트")
void when_join_not_error() {
    Wedul wedul = new Wedul();

    assertEquals(wedul.getId(), 1L, "아이디가 다릅니다.");
}

image

하지만 이렇게 작업 하는 것 보다 테스트를 편하게 도와주는 AssertJ 라이브러리를 사용해서 테스트 하면 더욱 편하다. 실제로 이 방식으로 팀에서도 하고 있어서 사용하는데 편했다.

assertThat(wedul.getId).isEqual(1L);

isEqual 이외에도 isNotNull, isNull 등 다양하게 확인이 가능하다.


특정 조건이 맞는 경우만 테스트 진행
assumeTrue, assumingThat를 사용하여 조건이 일치 할 때만 테스트를 진행하도록 할 수 있다.

// 시스템 환경설정의 profile이 dev일 때만 밑에 기능 테스트 가능!
assumeTrue("dev".equalsIgnoreCase(profile));

// assumingTest를 통해 특정 조건이 가능했을 때, 다음파라미터의 테스트 가능
assumingThat("test".equals("test"), () -> {
assertThat(study.getLimit()).isEqualTo(0); });

만약 조건이 맞지 않으면 다음과 같이 테스트가 중지된다.
image


특정 동작하는 테스트들만 그룹화하여 테스트 하기
테스트 코드를 만들었을 때 특정 동작을 하는 테스트 코드들만 그룹화하고 필요 시 이들만 테스트 하고 싶을 수 있다. 이때 @Tag("그룹명")을 통해 그룹화 할 수 있다.

@Test
@DisplayName("가입 오류 테스트")
@Tag("quick")
void when_join_not_error() {}

위와 같이 @Tag로 묶고 해당 태그가 붙은 테스트만 실행 시키고 싶은 경우에 Run/Debug Configurations 다이얼로그를 띄우고 Test Kind를 Tag로 변경한 뒤 Tag Expression에 테스트 하고자하는 태그명을 적어주면 된다.
image


커스텀 태그 만들기
test를 위해서 태그를 붙이다 보면 동일한 동작을 하는 test method들에 동일한 태그를 반복해서 붙여줘야할 때가 있다. 아주 귀찮다. 이를 해결하기 위해 커스텀 태그를 만들어서 사용할 수 있다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("quick")
@Test
public @interface QuickTag {
}

// 이렇게 공통 태그를 묶어서 커스텀 태그를 만들 수 있다.
@DisplayName("가입 오류 테스트")
@QuickTag()
void when_join_not_error() {
    Wedul wedul = new Wedul();

    assertEquals(wedul.getId(), 1L, "아이디가 다릅니다.");
}


반복 테스트하기
테스트를 몇회 이상 반복하고 싶을 때 @RepeatedTest 어노테이션을 사용해서 테스트 할 수 있다. 그리고 RepetitionInfo를 매개변수로 받아 현재 반복 정보를 확인 할 수 있다. 그리고 name에 별도 인자값을 주어 현재 반복 테스트 정보를 이용하여 테스트 이름을 만들 수 있다.

@DisplayName("반복 테스트")
@RepeatedTest(value = 10, name = "{currentRepetition}/{totalRepetition} {displayName}")
void repeatTest(RepetitionInfo repetitionInfo) {
    System.out.println("반복 테스트");
}

image


Parameter를 받아서 테스트 하기
테스트를 진행할 parameter를 받아서 테스트를 진행 할 수 있다. 이때 파라미터로 보내는 메서드는 static하고 이름이 같아야 한다.

@ParameterizedTest(name = "{index} {displayName} message={0}")
@MethodSource()
void parameterTest(String str) {
    System.out.println(str);
}

static Stream<Arguments> parameterTest() {
    return Stream.of(
      Arguments.of("cjung"),
        Arguments.of("wedul")
    );
}

마찬가지로 @ParameterizedTest value에 이름을 index, index 등으로 테스트 정보를 기입할 수 있다.
image


Parameter 조작하여 테스트 하기
테스트에 들어오는 parameter를 조작하여 사용할 수 있다. 우선 SimpleArgumentConverter를 사용하면 들어온 데이터를 바로 다른 타입으로 변경해서 파라미터로 사용할 수 있게 해준다.

@ParameterizedTest(name = "{index} {displayName} message={0}")
@MethodSource()
void parameterConvertTest(@ConvertWith(WedulConverter.class) Wedul wedul) {
    System.out.println(wedul.getId());
}

static Stream<Arguments> parameterConvertTest() {
    return Stream.of(
        Arguments.of("1"),
        Arguments.of("2")
    );
}

static class WedulConverter extends SimpleArgumentConverter {
    @Override
    protected Object convert(Object source, Class<?> targetType) throws ArgumentConversionException {
        assertThat(source.getClass()).isEqualTo(String.class);
        return new Wedul(Long.parseLong(source.toString()));
    }
}

그리고 Aggregator를 이용하여 들어온 파라미터들을 합쳐서 파라미터를 제공해줄 수 있다.

@ParameterizedTest(name = "{index} {displayName} message={0}")
@MethodSource()
void parameterAggregatorTest(@AggregateWith(WedulAggregator.class) Wedul wedul) {
    System.out.println(wedul.getId());
    System.out.println(wedul.getBalance());
}

static Stream<Arguments> parameterAggregatorTest() {
    return Stream.of(
        Arguments.of("1", 21),
        Arguments.of("2", 41)
    );
}

static class WedulAggregator implements ArgumentsAggregator {
    @Override
    public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
        return new Wedul(accessor.getLong(0), accessor.getInteger(1));
    }
}


테스트 인스턴스
테스트 간의 의존관계가 있기 때문에 클래스에 있는 모든 테스트들은 서로 다른 객체에서 실행된다. 그렇기 때문에 테스트에서 클래스 내부에 있는 인스턴스를 접근해서 값을 변경해도 다른 테스트에서 해당 데이터를 접근하면 기존 값으로 되어있다. 진짜 그런지 인스턴스 변수 값을 조작해서 찍어보고 객체의 hash값을 찍어보자.

public class WedulTestInstance {

    int value = 1;

    @Test
    void test_1() {
        System.out.println(this);
        System.out.println(value++);
    }

    @Test
    void test_2() {
        System.out.println(this);
        System.out.println(value++);
    }

}

image

해시값도 같고 인스턴스 변수도 변하지 않는다는 걸 볼수 있다. 이를 해결하기 위해서 @TestInstance(TestInstance.Lifecycle.PER_CLASS)를 클래스에 지정하여 클래스당 인스턴스를 하나만 만들게 할 수 있다. 이러면 value도 변하고 해시값도 같은 걸 확인할 수 있다.
image


테스트 순서
테스트의 순서를 경우에 따라 지정하고 싶은 경우에는 클래스에 어노테이션으로 @TestMethodOrder를 사용하여 지정하는데 그 구현체로는 MethodOrder에는 OrderAnnotation, Alphanumeric, Random이 존재한다. 그리고 각 메서드에 @Order(순서)를 지정하여 진행할 수 있다.

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestOrder {

  @Test
  @Order(0)
  void second() {
      System.out.println(1);
  }

  @Test
  @Order(1)
  void first() {
      System.out.println(2);
  }

}


테스트 전역 properties
전역으로 설정가능한 테스트 속성을 추가하여 사용할 수 있다. 위치는 test/resources에 junit-Platform.properties 파일을 만들어서 진행하고 인텔리제이에 resources를 추가한다. 만약 메소드에 더 우선순위가 높은 애노테이션이 붙어있으면 그 설정이 우선이 된다.
image

## 대표 설정 값들
// 클래스마다 하나의 인스턴스 생성 (적용)
junit.jupiter.testinstance.lifecycle.default = per_class

// Disabled 무시하고 실행하기
junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition


Junit5 확장팩
Junit5에서 사용 할 확장 모델을 만들어서 테스트를 편하게 만들 수 있다. 사용할 때는 클래스에 @ExtendWith(확장팩 클래스.class)를 통해서 진행할 수 있다. 확장팩에 사용할 수 있는 리스트는 다음과 같다.
image
Extension을 만들고 사용하는 부분은 이곳 참조.
https://github.com/weduls/junit5/blob/master/java-junit5-study-junit/src/test/java/com/wedul/javajunit5studyjunit/extensions/FindSlowTestExtension.java


변경된 기능

Junit4에서는 @Before, @BeforeClass, @After, @AfterClass를 사용하여 테스트 사용 전, 후에 대하여 setup등을 진행하였다. Junit5에서도 동일한 기능을 제공하는데 이름만 변경되었다.

Junit4 Junit5 기능
@Before @BeforeEach 테스트 마다 실행되기전 실행
@BeforeClass @BeforeAll 테스트 클래스 당 테스트 전 실행되는 메서드, static 메서드 (Test Instance 전략 변경 시 non static 가능)
@After @AfterEAch 테스트 마다 실행된 후 실행
@AfterClass @AfterAll 테스트 클래스 당 테스트 후 실행되는 메서드, static 메서드 (Test Instance 전략 변경 시 non static 가능)



출처 : https://www.inflearn.com/course/the-java-application-test/#

Github : https://github.com/weduls/junit5


댓글()

nodejs 비동기 프로그래밍을 위한 deferred

web/javaScript|2018. 10. 6. 23:57

node.js에서 비동기 프로그래밍을 위해서 사용할 수 있는 deferred 라이브러리를 정리해보자.


우선 deferred의 경우에는 이전글에 작성했던 Promise와 동일한 개념이다. (https://wedul.tistory.com/508) promise와 마찬가지로 deferred는 비동기로 작업을 진행하고 비동기 처리가 완료되고 resolve, reject 메소드를 실행해서 비동기 동작 이후에 결과를 전달 할 수 있다.


우선 필요한 라이브러리를 다운로드 받아보자. 

https://www.npmjs.com/package/deferred

1
npm i deferred
cs


그리고 라이브러리 예제에 나와있는대로 deferred 라이브러리를 사용해서 비동기 프로그램을 실행시켜보면 간단하게 promise와 동일 한 방식으로 진행할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
'use strict';
 
const deferred = require('deferred');
const promiseTest = require('./promise-test');
const deferredTest = module.exports;
 
deferredTest.deferTest = () => {
  return () => {
    const def = deferred();
 
    setTimeout(async () => {
      try {
        let dd = await promiseTest.proTest(true);
 
        if (dd.message == 'test') {
          def.reject('Error');
        } else {
          def.resolve(dd);
        }
      } catch (e) {
        def.reject(e);
      }
    }, 1000);
 
    return def.promise;
  };
};
 
 
 
// 테스트 
const deferredTest = require('../../lib/libraryTest/deferred-test');
 
 it('deferred Test', async () => {
    try {
      let dd = await deferredTest.deferTest()();
      console.log(dd);
    } catch (e) {
      console.log(e);
    }
 
  });
cs


댓글()

router 경로 enumset 처럼 받는 방법과 테스트 사이트

web/node.js|2018. 10. 5. 00:20

router.get('/wedul/:name(cjung|gglee), () => {
});

이런식으로 기재되어 있는 router 주소 매핑을 책에서 봤다. 자세한 설명이 없어서 알아보던 중 /wedul/ 주소 다음에 cjung 또는 gglee가 올 수 있는 형태라고 한다.

이걸 찾아보면서 router 경로를 만들고 테스트 까지 해볼 수 있는 사이트를 찾았다.


http://forbeslindesay.github.io/express-route-tester/


Route에 규칙을 추가하고 path에 테스트할 규칙을 입력하면 key와 RegExp 그리고 결과까지 확인 할 수 있도록 제공해준다.

좋다.



댓글()

node.js에 swagger 적용

web/node.js|2018. 10. 5. 00:13

Spring Boot에 적용했었던 swagger를 node.js에도 적용해보자.

spring boot에서는 자동으로 만들어졌으나, node.js에서는 Definition을 적용해줘야해서 귀찮다.
설정 방법을 알아보자.

설치 패키지

1
2
"swagger-jsdoc": "^3.0.2",
"swagger-ui-express": "^4.0.0"
cs



Definition정의

swagger에 대해 적용할 프로그램에 대한 정보와 path, api들 위치등에 대해 정의한 definition을 정의한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
 * Created by wedul on 2018. 8. 30.
 */
'use strict';
 
module.exports = {
  swaggerDefinition: {
    // 정보
    info: {
      title: 'node js test app',
      version: '1.0.0',
      description: 'Make For node js test.'
    },
    // 주소
    host: "localhost:3000",
    // 기본 root path
    basePath: "/",
    contact: {
      email: "rokking1@naver.com"
    },
    // 각 api에서 설명을 기록할 때 사용할 constant들을 미리 등록해놓는것
    components: {
      res: {
        BadRequest: {
          description: '잘못된 요청.',
          schema: {
            $ref: '#/components/errorResult/Error'
          }
        },
        Forbidden: {
          description: '권한이 없슴.',
          schema: {
            $ref: '#/components/errorResult/Error'
          }
        },
        NotFound: {
          description: '없는 리소스 요청.',
          schema: {
            $ref: '#/components/errorResult/Error'
          }
        }
      },
      errorResult: {
        Error: {
          type: 'object',
          properties: {
            errMsg: {
              type: 'string',
              description: '에러 메시지 전달.'
            }
          }
        }
      }
    },
    schemes: ["http""https"], // 가능한 통신 방식
    definitions:  // 모델 정의 (User 모델에서 사용되는 속성 정의)
      {
        'User': {
          type: 'object',
          properties: {
            id: {
              type: 'string'
            },
            age: {
              type: 'integer'
            },
            addr: {
              type: 'string'
            }
          }
        }
      }
  },
  apis: ['./routes/**/*.js'// api 파일 위치들 
};
cs


api 주소
각 라우터에 대한 정보를 적어주어야 swagger-ui에서 정의된대로 나온다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
 * @swagger
 * tags:
 *   name: User
 *   description: 사용자 정보 가져오기
 */
module.exports = router;
 
/**
 * @swagger
 * /user/:
 *   get:
 *     summary: 사용자 정보 가져오기
 *     tags: [User]
 *     parameters:
 *       - in: query
 *         name: id
 *         type: string
 *         enum: [cjung, gglee, etc..]
 *         description: |
 *          사용자 아이디 전달
 *     responses:
 *       200:
 *         description: 성공
 *       403:
 *         $ref: '#/components/res/Forbidden'
 *       404:
 *         $ref: '#/components/res/NotFound'
 *       500:
 *         $ref: '#/components/res/BadRequest'
 */
router.get('/', async (req, res, next=> {
  const {id} = req.query;
  let data = await userService.findUser(new UserDto.ParamBuilder(id).build());
 
  if (data) {
    res.json(data);
  } else {
    return next(new NotFoundError('잘못된 요청입니다.'));
  }
});
cs


app.js
app.js에 Definition을 정의하고 swaggerSpec이랑 swaggerUI관련 설정을 해주면된다.

1
2
3
4
5
6
const swaggerJSDoc = require('swagger-jsdoc');
const swaggerOption = require('./routes/swagger');
const swaggerSpec = swaggerJSDoc(swaggerOption);
const swaggerUi = require('swagger-ui-express');
 
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
cs


접속
http://localhost:3000/api-docs/ 에 접속하면 접속한 화면이 나온다.


#
설정한 router 정보

각 router에서 정의한 api 주소 정보가 확인된다.


또한 테스트도 진행할 수 있다. 파라미터 값을 선택하고 execute 버튼을 누르면 실제 테스트를 진행하고결과까지 보여 준다.


그리고 함께 정의하였던 response 정보도 확인할 수 있다.


마지막으로 Definion 문서 마지막에 정의하였던 각 Model의 Definition도 확인할 수 있다. 


자세한 문서는 여기 참고
https://frontalnh.github.io/2017/11/22/nodejs-swagger-api-doc-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0/

https://github.com/swagger-api/swagger-editor


위의 예제 코드는 github 참고
https://github.com/weduls/nodejs_test


## 참고로 swagger를 쓰지 않고 테스트에서 super-test 라이브러리를 사용해서 테스트할수도 있다. 하지만 ui가 없고 front나 다른 개발자한테 api 정보를 넘겨주고 테스트까지 가능하게 해주는 swagger가 좋은거 같다. (super-test는 하단의 링크 참고)


댓글()
  1. ㅇㅇ 2019.01.16 16:02 댓글주소  수정/삭제  댓글쓰기

    위의 예제코드 깃헙 링크가 404 not found 뜹니다..

  2. lll 2019.10.09 15:10 댓글주소  수정/삭제  댓글쓰기

    에구. ㅠㅠ 혹시 다시 예제를 올려주실 생각 없나요? 설명 잘 적으셨지만, 그래도 소스로 참고하고 싶네요

테스트 모듈 Assert 정리

web/node.js|2018. 10. 5. 00:04

Assert는 node.js의 단위 테스트를 하기위해서 제공되는 테스트 모듈이다. 해당 모듈의 주요 메소드를 정리해보자.


Assert 메소드 정리

// 모듈 로드
const assertion = require('assert');

assertion.ok(object)
인자값이 true 아니면 에러
assertion.ifError(object)
인자값이 false 아니면 에러
assertion.equal(object, object) 
두 개의 인자를 == 으로 비교 
assertion.notEqual(object, object)
두 개의 인자를 != 으로 비교
assertion.staticEqual(object, object)
두 개의 인자를 === 으로 비교
assertion.notStaticEqual(object, object)
두 개의 인자를 !== 으로 비교
assertion.deepEqual(object, object)
객체를 그냥 equal 하게 되면 객체를 참조하고 있는 주소가 서로 다르기 때문에 무조건 false가 나온다. 그래서 해당 객체의 속성값을 == 으로 비교해주는 메소드
assertion.notDeepEqual(object, object)
deepEqual 메소드를 != 으로 비교하는 메서드
assertion.deepStrictEqual() 
deepEqual 메소드를 === 으로 비교하는 메서드
assertion.notDeepStrictEqual() 
deepEqual 메소드를 !== 으로 비교하는 메서드
assertion.throws(() => new Error(‘에러’), TypeError) 
인자로 넘어온 값이 함수이면서, 반환값이 에러인경우 ( 두 번째 인자는 오류의 유형인데 option)
assertion.doesNotThrow(() => new Error(‘에러’), TypeError)
인자로 넘어온 값이 함수이면서, 반환값이 에러가 아닌 경우 ( 두 번째 인자는 오류의 유형인데 option)
assertion.fail() 
무조건 실패


댓글()

Node js 테스트 프레임워크 Mocha

web/node.js|2018. 10. 4. 23:57

회사에서 node.js를 이용해서 백엔트 프로젝트에 참여하게 되어서 새롭게 node.js를 공부하게 되었다.

그러면서 node.js에서 새롭게 사용할 테스트 프레임워크를 찾던중 mocha라는 것을 발견했다.


Mocha
- node.js에서 사용하는 테스트 프레임워크로써 suite를 만들어서 unit 테스트를 가능하도록 제공해주는 프레임워크이다.


설치방법
mocha는 Node.js 6.x 버전 이상이부터 지원한다.

1
2
3
npm i mocha -g
 
npm i --save-dev mocha
cs

package.json에 해당 스크립트를 추가해준다.

1
2
3
"script" : {
  "test" : "mocha"
}
cs


사용방법

root 경로에 test directory를 생성하고 javascript를 추가한다. 그리고 describe와 it 메서드를 사용하여 테스트를 진행한다.

describe : 테스트의 suite()를 구성하기 위해 사용되는 집합 (모집군으로써 집단을 만드는데 사용)
it : 실제로 테스트를 진행하는 메서드


이런식으로 간단하게 지정하여 테스트를 진행할 수 있다.

몇 가지 추가기능을 더 설명하자면 같은 suite() 내부에서 테스트를 제외하고 싶은 경우에는 it.skip()을 사용하고 별도의 테스트만 진행하고 싶을 때는 it.only()를 사용한다.

Junit처럼 별도의 assert를 제공하는데 이는 다음 장에 정리해보자.




댓글()

Fake SMTP로 메일 전송 테스트

IT 지식/기타지식|2018. 10. 4. 23:51

간단하게 rabbitMQ 코드 만들어보는 도중에 메일전송기능이 필요했다.

그런데 메일을 계속 보낼수도 없기 때문에 메일이 잘 전송되고 있는지 확인할 수 있는 프로그램이 있는지 찾아봤다. 


그중에 Fake SMTP 라는 프로그램이 있어서 소개해본다.

프로그램은 하단에 첨부된 링크에서 다운받을 수 있다. 실행방법은 다운받은 파일위치에서  java -jar fakeSMTP-2.0.jar 명령어로 실행시키면 GUI 화면이 나온다.

스프링 부트에서 JavaMailSender와 간단한 설정을 통해 메일 전송 여부를 테스트 할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// application.properties
spring.mail.host=localhost
spring.mail.port=2525
 
// service
/**
 * microservice
 *
 * @author wedul
 * @since 2018. 8. 25.
 **/
@Component
public class Mailer {
 
    @Autowired
    private JavaMailSender javaMailSender;
 
    public void sendMail(String email) {
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setTo(email);;
        mailMessage.setSubject("Registration");
        mailMessage.setText("Successfully Registered");
        javaMailSender.send(mailMessage);
    }
}
 
cs


http://nilhcem.com/FakeSMTP/download.html#

댓글()