Today I Learned. by Rio

Custom assertions in AssertJ

|

자동화 테스트 지원을 하며 AssertJ 라이브러리의 custom assertion을 만들어서 사용하였는데 사용하면서 느낀 점을 정리해본다. 작성 방법은 공식 홈페이지에 워낙 쉽게 설명되어 있기 때문에(Creating assertions specific to your classes) 이 포스트에서는 따로 설명하지 않는다.

코드 가독성이 좋아진다.

내가 느낀 제일 가장 큰 장점은, custom assertion을 만들어서 사용한 경우 크게 두 가지 케이스에 대해서 코드 가독성이 눈에 띄게 좋아졌다는 점이다.

우선, 객체 내부의 여러 필드에 대해 값을 검증하는 경우 가독성이 좋아지는 케이스이다. 아래의 코드를 보면 확실하게 느껴진다. Customer라는 객체 내부에 여러 필드를 검증하는 예시를 간단하게 요약해 표현했다.

public class Customer {

    private int customerId;
    private String name;
    private int age;
    private String email;
    private String phoneNumber;
    private List<Discount> discountList;

    public static class Discount {
        private int discountId;
        private String name;
    }
}

이 경우 전체 필드의 값에 대한 검증을 할 수 있는데 일반적인 AssertJ 라이브러리를 활용하면 아래처럼 쓸 수 있다.

@Test
public void customerTest() {

    Customer rio = getCustomerById(231);

    assertThat(rio.getCustomerId()).isEqualTo(231);
    assertThat(rio.getName()).isEqualTo("rio");
    assertThat(rio.getAge()).isEqualTo(33);
    assertThat(rio.getGender()).isEqualTo(Gender.male);
    assertThat(rio.getAddress()).isEqualTo("서울");
    assertThat(rio.getEmail()).isEqualTo("rio.dykim@gmail.com");
    assertThat(rio.getPhoneNumber()).isEqualTo("010-1111-2222");
    assertThat(rio.getDiscountList()).hasSize(2);
    List<String> discountNameList = rio.getDiscountList().stream().map(Customer.Discount::getDiscountName).collect(toList());
    assertThat(discountNameList).contains("연회비할인", "가족회원할인");
}

Custom assertion을 활용하는 경우 위 코드를 아래처럼 표현할 수 있다.

@Test
public void customerTest() {

    Customer rio = getCustomerById(231);

    CustomerAssert.assertThat(rio)
            .customerId(231)
            .name("rio")
            .age(33)
            .gender(Gender.male)
            .address("서울")
            .email("rio.dykim@gmail.com")
            .phoneNumber("010-1111-2222")
            .hasDiscountCount(2)
            .containsDiscount("연회비할인", "가족회원할인");

}

어떤 필드가 어떤 값을 가지는지 검증하고자 하는 의도가 코드에 자연스럽게 드러난다. 위의 코드와 완전히 같은 일을 하지만 가독성 측면에서 상당한 효과를 볼 수 있다.

그리고 다른 케이스에서 장점이라 볼 수 있는 점은 검증문 자체에 비즈니스 로직이 필요한 경우이다. 위 케이스의 예를 들면 필드에서는 age와 gender, address만 가지고 있고 서울 경기권에 사는 30대 남자인지 아닌지를 판단하는 비즈니스 로직이 중요한 부분이라면 테스트 코드에서는 그 부분을 따로 아래와 같이 검증해야 한다.

Customer rio = getCustomerById(231);
assertThat(rio.getAge()).isBetween(30, 39);
assertThat(rio.getGender()).isEqualTo(Gender.male);
assertThat(rio.getAddress()).isIn("서울", "경기");

또는 해당하는 조건들을 모아 변수로 뽑던지, private 메서드를 활용하던지 해서 true/false 형태의 검증도 가능하다.

boolean is30sManInMetropolitanArea = rio.getAge() >= 30 && rio.getAge() < 40
        && rio.getGender().equals(Gender.male)
        && (rio.getAddress().equals("서울") || rio.getAddress().equals("경기"));
assertThat(is30sManInMetropolitanArea).isTrue();

이런 경우 custom assertion을 활용하여 간단히 아래처럼 표현할수 있다. 메서드 체이닝을 이용해 다른 검증과 함께 할 수 있는 장점도 살릴 수 있다. 물론 내부 구현은 작성해야 하겠지만 테스트 코드를 읽어가는 흐름에서는 그 검증 로직이 코드에 노출되느냐 아니냐에 이슈를 두는 것이 아니기 때문에 큰 문제가 되지 않는다.

@Test
public void customerTest() {

    Customer rio = getCustomerById(231);

    CustomerAssert.assertThat(rio)
            .customerId(231)
            .is30sManInMetropolitanArea();
}

대부분의 프로젝트가 위의 예시보다는 더욱 복잡한 구조의 모델을 가질 테고, 케이스도 훨씬 다양할 것이다. 프로젝트의 규모가 커지고 기간이 길어질수록 코드 가독성은 곧 생산성과 직결되는 문제이다. 실제 지원했던 프로젝트의 보안 규정 때문에 복잡한 코드들을 포스트에 공개할 수는 없지만, 수많은 테스트코드를 작성하며 다른 사람이 짠 테스트 코드도 봐야 하는 개발자 입장에서는 이런 부분들이 굉장히 큰 편의로 다가오게 된다.

Custom logging filter in Rest-Assured

|

보통 Rest-Assured를 이용해서 api를 테스트 할 때 로깅 세팅을 한다. Request와 Response를 테스트 로그로 남기며 테스트를 진행하는데, 대부분 아래와 같이 세팅한다.

@Test
public void callGoogleBooksApi() {
    // https://www.googleapis.com/books/v1/volumes?q=flowers&filter=free-ebooks

    RequestSpecification requestSpecification = new RequestSpecBuilder()
            .setBaseUri("https://www.googleapis.com/books")
            .setBasePath("/v1")
            .setContentType(ContentType.JSON)
            .addFilter(new RequestLoggingFilter())
            .addFilter(new ResponseLoggingFilter())
            .build();

    given(requestSpecification)
            .queryParam("q", "flowers")
            .queryParam("filter", "free-ebooks")
            .get("volumes")
            .then().statusCode(200);
}

위와 같이 RequestSpecification 내부에 addFilter 함수를 이용하여 RequestLoggingFilter와 ResponseLoggingFilter를 설정해준다. 이 경우 Rest-Assured로 API 테스트를 진행하면 아래와 같이 console에 이쁘게 로그가 찍혀나온다.

image1

필터 설정하는 부분만 바꾸어가며 여러가지 로깅 설정이 가능하다. error가 있을 때만(status code가 400~500 사이의 경우) 전체 로그를 찍고 나머지 경우에는 로그가 나오지 않게끔 하기 위해 ErrorLoggingFilter를 사용하기도 한다.

RequestSpecification requestSpecification = new RequestSpecBuilder()
        .setBaseUri("https://www.googleapis.com/books")
        .setBasePath("/v1")
        .setContentType(ContentType.JSON)
        .addFilter(new ErrorLoggingFilter())
        .build();

이 경우에는, 시나리오상 호출하는 API 갯수가 많으면 테스트가 진행되는 동안 아무런 내용도 콘솔창에 찍히지 않기 때문에 답답함을 느낄 수도 있다. 그래서 심플한 로깅 필터를 사용해서 진행되는 내역을 콘솔에 찍어낼 수 있다. RequestLoggingFilter와 ResponseLoggingFilter 내에 파라미터로 들어가는 LogDetail 설정을 지정하면 된다.

RequestSpecification requestSpecification = new RequestSpecBuilder()
        .setBaseUri("https://www.googleapis.com/books")
        .setBasePath("/v1")
        .setContentType(ContentType.JSON)
        .addFilter(new ErrorLoggingFilter())
        .addFilter(new RequestLoggingFilter(LogDetail.METHOD))
        .addFilter(new RequestLoggingFilter(LogDetail.URI))
        .addFilter(new ResponseLoggingFilter(LogDetail.STATUS))
        .build();

이 경우 실제 콘솔창에는 아래처럼 호출하는 api의 메서드 종류(GET, POST 등등..), uri, 그리고 response의 status code를 콘솔창에 출력해준다. 또한 ErrorLoggingFilter도 설정했으므로 error가 발생하는 경우 해당 api의 request, response에 대한 상세한 로그를 출력해준다.

image2

진행했던 프로젝트에서는 하나의 테스트케이스를 돌리는 경우 20개 이상의 api가 시나리오대로 호출되는 경우들이 빈번했기 때문에 api 호출 1회당 3줄의 출력도 상당히 복잡하게 느껴졌다. 그래서 custom logging filter를 만들어 사용했다. 위와 같이 최소한으로 필요한 정보인 호출하는 api의 정보(uri, method)와 응답코드까지만 출력을 한줄로 끝내게 하기 위해 아래와 같은 방법을 활용했다.

RequestSpecification requestSpecification = new RequestSpecBuilder()
        .setBaseUri("https://www.googleapis.com/books")
        .setBasePath("/v1")
        .setContentType(ContentType.JSON)
        .addFilter(new CustomRequestLoggingFilter())
        .addFilter(new CustomResponseLoggingFilter())
        .addFilter(new ResponseLoggingFilter(LogDetail.STATUS))
        .build();

아래는 custom logging filter 내부 구성

public class CustomRequestLoggingFilter implements Filter {
    @Override
    public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
        System.out.print("[" + requestSpec.getMethod() + "]   " + requestSpec.getURI());
        return ctx.next(requestSpec, responseSpec);
    }
}

public class CustomResponseLoggingFilter implements Filter {
    @Override
    public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
        Response response = ctx.next(requestSpec, responseSpec);
        int statusCode = response.getStatusCode();
        if (statusCode >= 400 && statusCode <= 500) {
            System.out.println();
            ResponsePrinter.print(response, response, System.out, LogDetail.ALL, true);
        } else {
            System.out.println("  (" + response.getStatusCode() + ")");
        }
        return response;
    }
}

이 경우 아래처럼 api 1회 호출당 1줄의 콘솔 로그가 찍히고 오류가 나는 경우는 ErrorLoggingFilter 적용한 것과 같이 전체 로그가 찍히게 된다.

image3

상황에 맞게 필요한 로그를 사용하면 될 듯 하고, 많은 상황에서는 첫번째 전체 request와 response를 출력하는 설정을 쓰곤 한다.

Font ligatures in IntelliJ

|

IntelliJ 에디터 사용시 폰트 합자(?) 적용하는 방법에 대해서 적어본다. 좀 쓸데없는 팁이지만, 괜히 이쁘게 쓰는 방법 같아서 남겨봄.

Settings > Editor > Font > Enable font ligatures 체크

아래와 같이 연산자나 특수기호 등이 에디터에서 예쁘게 표시되게끔 바꾸어준다. 특히 ==, !=, >= 등의 비교 연산자나 && 등의 연결부호, 람다식에서의 arrow function(->) 등이 예쁘게 표시된다.

Enable font ligatures 체크 해제(적용 전)

not_check

Enable font ligatures 체크(적용 후)

check

자세히 보기 전에는 잘 모르는 부분들도 바뀌는데 주석 기호(//), 더하기2개(++), www 글씨, 복수 개의 파라미터를 받는 점 세개 varargs(…) 등이 바뀌었다. 기능적인 부분에는 전혀 변화가 없는, 팁이라고 하기도 좀 애매한 팁이지만 뭔가 예쁘게 인텔리제이를 쓰는 느낌이 듬.

Response parsing to class model in Rest-Assured

|

Rest-Assured 라이브러리를 사용해서 받은 response를 모델 객체로 파싱해서 쓸 때 아래와 같이 간단하게 쓸 수 있다.

MyClass myClass = RestAssured.given()
        .when().get("/blahblah")
        .then().statusCode(200)
        .extract().as(MyClass.class);

MyClass myClass = RestAssured.given()
        .when().get("/blahblah")
        .as(MyClass.class);

then()~extract() 구문은 빠져도 상관없다. then이후 특정 검증이 필요한 경우에 저런 식으로 사용하곤 했다. MyClass는 response를 분석해서 만들면 되고 ObjectMapper만 잘 설정해 두면 response를 굉장히 편하게 관리할 수 있다. 그 부분은 나중에 다시 정리.

ObjectMapper configuration in Rest-Assured

|

Rest-Assured 라이브러리를 사용해서 받은 response를 모델 객체로 파싱해서 쓰는 방법에 대해서 간단하게 남겼는데(“Response parsing to class model in Rest-Assured”) 내부적인 동작은 Rest-Assured가 가지고 있는 ObjectMapper가 response로 받은 json구조를 분석하여 Class에 자동으로 파싱해주는 것이다.

이때 어떤 api는 수많은 정보를 response에 전달하고, 사용하는 측에서는 response의 모든 값이 아니라 필요한 값만 클래스에 담고 싶은 경우가 있다.

이 경우에는 아래와 같이 ObjectMapper를 만들어 RestAssured에 설정해줄 수 있다.

ObjectMapper objectMapper = new ObjectMapper()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

RestAssuredConfig restAssuredConfig = new RestAssuredConfig()
        .objectMapperConfig(new ObjectMapperConfig().jackson2ObjectMapperFactory((cls, charset) -> objectMapper));

given().config(restAssuredConfig)
        .get("/blahblah")
        .then().statusCode(200)
        .body("name", is("rio"));

원하는 설정을 담은 custom한 objectMapper를 만든 후 RestAssuredConfig에 세팅한다. 그 이후 실제로 call하는 rest-assured 코드에서는 .config() 메서드에 미리 만들어둔 RestAssuredConfig를 담아서 호출한다.

참고로 저 위의 DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES 설정은 json을 class로 파싱하면서 모르는 프로퍼티가 나와도 무시하라는 뜻이다. 이 경우 response에 많은 정보가 담겨나와도 내가 원하는 class에 파싱하면서 class에 필요한 정보만 찾아 넣어주기 때문에 클래스 구조를 심플하게 가져갈 수 있다.

또, MapperFeature.USE_ANNOTATIONS 피쳐도 테스트 진행시 많이 사용했던 설정중 하나이다. 이는 실제 제품코드에서 사용하는 모델 객체에 필요해서 달아둔 어노테이션 설정을 다 무시하는 세팅이다.

objectMapper 설정을 통해 피쳐들을 disable하거나 enable할 수 있으므로 검색해보고 판단해서 쓰면 좋은 효과를 볼 수 있다.

다음과 같이 여러 피쳐들을 한번에 disable할 수도 있다.

ObjectMapper objectMapper = new ObjectMapper()
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .disable(MapperFeature.USE_ANNOTATIONS);

다음과 같이 호출되는 Rest-Assured 전역 설정으로 사용할 수도 있다.

RestAssured.config = restAssuredConfig;
참고) Jackson2ObjectMapperBuilder

스프링 기반 프로젝트의 테스트에서는 아래와 같이 Jackson2ObjectMapperBuilder를 사용할 수 있다. 이유는 잘 모르겠지만 Jackson2ObjectMapperBuilder 클래스는 org.springframework:spring-web 라이브러리 안에 속해있다. 이름만 봐서는 당연히 jackson-databind 라이브러리에도 있을 것 같지만 없다. 특별한 경우가 아니면 쓸 일이 없지만 진행했던 프로젝트에서는 아래와 같이 특정 클래스의 serializer 또는 deserializer를 전역 설정으로 쓸 수 있어서 굉장히 편하게 사용했다.

RestAssuredConfig restAssuredConfig = new RestAssuredConfig().objectMapperConfig(new ObjectMapperConfig()
        .jackson2ObjectMapperFactory((cls, charset) -> {
                    ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder()
                            .serializerByType(MyRequest.class, new MyRequestSerializer())
                            .deserializerByType(MyResponse.class, new MyResponseDeserializer())
                            .featuresToDisable(...your features...)
                            .build();
                    return objectMapper;
                }
        ));