Automated Testing for OpenAPI Endpoints Using CATS

1. 소개

이 튜토리얼에서는 CATS를 사용하여 OpenAPI로 구성된 REST API의 테스트를 자동화하는 방법을 탐구합니다. API 테스트를 수작업으로 작성하는 것은 지루하고 시간 소모가 많지만, CATS는 수백 개의 테스트를 자동으로 생성하고 실행함으로써 이 과정을 간소화합니다.

이는 수작업의 노력을 줄이고, 개발 초기 단계에서 잠재적인 문제를 식별함으로써 API의 신뢰성을 향상시킵니다. 간단한 API의 경우에도 일반적인 오류가 발생할 수 있으며, CATS는 효율적으로 이를 찾아내고 해결하는 데 도움을 줍니다.

CATS는 OpenAPI 주석이 있는 모든 애플리케이션과 함께 사용할 수 있지만, 이번에는 Spring과 Jackson 기반 애플리케이션을 사용하여 시연할 것입니다.

2. CATS로 쉽게 테스트하기

CATS는 계약 자동 테스트 서비스(Contract Auto Test Service)의 약어입니다. 여기서 계약은 REST API의 OpenAPI 사양을 의미합니다. 자동 테스트는 모호한 테스트(fuzz testing)로, 랜덤 데이터와 일부 시나리오(예: ID)에서 API 작업에 의해 반환된 데이터로 구성됩니다. 이것은 우리의 API URL 및 OpenAPI 계약(파일 또는 URL 형태)에 접근이 필요한 외부 CLI 애플리케이션입니다.

주요 기능으로는 다음이 있습니다:

  • API 계약에 기반한 테스트 자동 생성 및 실행
  • 테스트 결과를 자세히 설명하는 HTML 보고서 자동 생성
  • 인증 요구 사항에 대한 간편한 구성

테스트는 자동으로 생성되므로 OpenAPI 사양을 변경할 때 생성기를 다시 실행하는 것 외에는 유지 관리가 필요 없습니다.

이는 많은 엔드포인트를 가진 API에서 특히 유용합니다. 또한 모호한 테스트(fuzzing)를 포함하기 때문에 최초에는 고려하지 않았을 테스트를 생성합니다.

2.1. CATS 설치

우리는 몇 가지 설치 옵션을 가지고 있습니다.

가장 간단한 방법은 JAR 또는 이진 파일을 다운로드하여 실행하는 것입니다. 우리는 이진 옵션을 선택할 것이며, 이 방법은 Java가 설치되고 구성된 환경을 요구하지 않아 어디에서나 테스트를 실행하기 쉽습니다.

다운로드 후, cats 바이너리를 우리의 환경 경로에 추가하여 어디에서나 실행할 수 있도록 해야 합니다.

2.2. 테스트 실행

테스트를 실행하려면 적어도 두 개의 인자를 지정해야 합니다: contractserver. 우리의 경우 OpenAPI 사양 URL은 /api-docs입니다:

$ cats --contract=http://localhost:8080/api-docs --server=http://localhost:8080

또한 사양을 포함하는 JSON 또는 YAML 로컬 파일을 계약으로 전달할 수도 있습니다.

이 파일이 CATS를 실행하는 동일한 디렉토리에 있다고 가정해 보겠습니다:

$ cats --contract=api-docs.yml --server=http://localhost:8080

기본적으로 CATS는 사양의 모든 경로에서 테스트를 실행하지만 패턴 매칭을 통해 몇 개의 경로로 제한할 수도 있습니다:

$ cats --server=http://localhost:8080 --paths="/path/a*,/path/b"

이 매개변수는 광범위한 사양에서 한 번에 몇 개의 경로에 집중할 때 유용할 것입니다.

2.3. 인증 헤더 포함

보통 우리의 API는 어떤 형태의 인증으로 보안됩니다. 이 경우, 명령에 인증 헤더를 포함할 수 있습니다. Bearer 인증을 사용할 때의 모습은 다음과 같습니다:

$ cats --server=http://localhost:8080 -H "Authorization=Bearer a-valid-token"

2.4. 보고서 생성

실행 후, 로컬에 HTML 보고서를 생성합니다:

CATS 보고서이후, 우리는 우리의 코드를 리팩토링하는 방법을 보기 위해 일부 오류를 검토할 것입니다.

3. 프로젝트 설정

CATS를 소개하기 위해, 우리는 @RestController 및 Bearer 인증을 사용하는 간단한 REST CRUD API로 시작할 것입니다. @ApiResponse]() 주석을 포함하는 것이 필수적입니다. 이 주석은 CATS가 사용하는 OpenAPI 정의의 중요한 세부 정보를 포함하고 있습니다. 예를 들어 미디어 타입 및 인증되지 않은 요청에 대한 예상 상태 코드는 다음과 같습니다:

@RestController
@RequestMapping("/api/item")
@ApiResponse(responseCode = "401", description = "Unauthorized", content = { 
  @Content(mediaType = MediaType.TEXT_PLAIN_VALUE, schema =
    @Schema(implementation = String.class)
  ) 
})
public class ItemController {

    private ItemService service;

    // endpoints ...
}

우리의 요청 매핑은 최소한의 Swagger 주석만 정의되어 있으며 가능하면 기본값에 의존합니다:

@PostMapping
@ApiResponse(responseCode = "200", description = "Success", content = {
  @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema =
    @Schema(implementation = Item.class)
  )
})
public ResponseEntity<Item> post(@RequestBody Item item) {
    service.insert(item);
    return ResponseEntity.ok(item);
}

// GET 및 DELETE 엔드포인트 ...

우리의 페이로드 클래스에는 몇 가지 기본 속성을 포함합니다:

public class Item {

    private String id;
    private String name;
    private int value;

    // 기본 getter 및 setter...
}

4. 보고서에서의 일반 오류 분석

보고서에서 얻은 일부 오류를 분석하여 해결하겠습니다. 각 필드에 대해 여러 유사한 테스트가 수행되므로 하나의 페이지에 대한 자세한 내용만 보여주겠습니다.

4.1. 추천 보안 헤더 누락

OWASP에서 권장하는 보안 헤더 세트가 있습니다. 보고서의 상세 테스트 페이지는 기본적으로 포함해야 할 헤더를 보여줍니다:

CATS 보안 헤더 Spring Security는 이러한 모든 헤더를 기본적으로 포함하므로, 우리의 프로젝트에 spring-boot-starter-security를 추가합시다:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.3.2</version>
</dependency>

특별한 구성 없이 우리의 SecurityFilterChain에서 보안 헤더를 포함할 수 있으므로, 우리는 유효한 토큰을 사용할 수 있도록 JWT 구성을 간단히 정의합니다:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
          .oauth2ResourceServer(rs -> rs.jwt(jwt -> jwt.decoder(jwtDecoder())))
          .build();
    }
}

jwtDecoder() 메서드의 구현은 우리의 요구 사항에 따라 달라집니다. 우리는.authorization 헤더를 사용하는 다른 인증 방법을 사용할 수 있습니다.

4.2. 요청 필드에서 매우 큰 값 또는 경계 밖의 값 전송

필드에 최대 길이가 지정된 경우, CATS는 더 큰 값을 전송하고 서버가 이러한 요청을 4XX 상태로 거부할 것으로 기대합니다. 최대 길이는 지정되지 않은 경우 기본값으로 만 개가 사용됩니다:

CATS 테스트 마찬가지로, CATS는 매우 큰 값을 가진 요청을 전송하며 기대는 동일합니다:

CATS 테스트 먼저, 이러한 문제를 해결하기 위해 애플리케이션에서 사용하는 ObjectMapper를 사용자 정의합시다.

JsonFactoryBuilder는 문자열에 대한 최대 길이를 포함한 몇 가지 제약을 설정할 수 있는 StreamReadConstraints 구성을 포함하고 있습니다. 최대 길이를 100으로 정의합시다:

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        JsonFactory factory = new JsonFactoryBuilder()
          .streamReadConstraints(
            StreamReadConstraints.builder()
            .maxStringLength(100)
            .build()
          ).build();

        return new ObjectMapper(factory);
    }
}

물론, 이 최대 길이는 애플리케이션의 요구 사항에 따라 달라집니다. 가장 중요한 것은, 이는 우리의 애플리케이션이 과도한 요청을 받지 못하도록 방지하지만 API 사양의 제약을 정의하지는 않습니다.

이를 위해, 페이로드 클래스에 몇 가지 유효성 검증 주석을 포함할 수 있습니다:

@Size(min = 37, max = 37)
private String id;

@NotNull
@Size(min = 1, max = 20)
private String name;

@Min(1)
@Max(100)
@NotNull
private int value;

다시 말하지만, 여기의 값은 우리의 요구 사항에 따라 달라질 것입니다. 그러나 이러한 경계를 포함시키는 것은 CATS의 테스트 생성 방법을 정의하는 데 도움이 됩니다. 마지막으로, 잘못된 요청을 거부하기 위해 POST 메서드를 @Valid 주석을 사용하도록 수정하겠습니다:

ResponseEntity<Item> post(@Valid @RequestBody Item item) { 
    //... 
}

4.3. 잘못 형성된 JSON 및 더미 요청

기본적으로 Jackson은 요청에 대해 매우 관대하며, 일부 잘못 형성된 JSON도 수용합니다:

CATS 테스트이를 방지하기 위해, 우리는 JacksonConfig로 돌아가 후행 토큰에서 실패하는 옵션을 활성화합시다:

mapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);

Jackson은 또한 Item 클래스에 없는 필드를 혼합하여 포함하는 요청과 더미 요청빈 JSON 본문도 수용합니다. 우리는 이러한 요청을 거부하기 위해 비정의 속성에서 실패하도록 강제할 수 있습니다:

mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

4.4. 정수에서 소수점 수

Jackson은 int 속성이 있을 때 소수값을 잘라냅니다:

CATS 테스트예를 들어, 값이 0.34인 경우 0으로 잘리게 됩니다. 이를 방지하기 위해 이 기능을 끕시다:

mapper.disable(DeserializationFeature.ACCEPT_FLOAT_AS_INT);

4.5. 값에서 제로 너비 문자

일부 모호한 테스트 도구는 필드 이름 및 값에 제로 너비 문자를 포함합니다:

CATS 테스트우리는 이미 FAILONUNKNOWN_PROPERTIES를 활성화했으므로, 일부 정리 작업을 포함하고 값의 제로 너비 문자를 제거해야 합니다. 우리는 이를 위해 커스텀 JSON 역직렬화기를 사용하고, 여기에서 일부 제로 너비 문자를 위한 정규 표현식 패턴을 정의하는 유틸리티 클래스를 만듭니다:

public class RegexUtils {

    private static final Pattern ZERO_WIDTH_PATTERN = 
      Pattern.compile("[\u200B\u200C\u200D\u200F\u202B\u200E\uFEFF]");

    public static String removeZeroWidthChars(String value) {
        return value == null ? null
          : ZERO_WIDTH_PATTERN.matcher(value).replaceAll("");
    }
}

먼저, 이것을 커스텀 역직렬화기에서 사용하여 String 필드를 처리합니다:

public class ZeroWidthStringDeserializer extends JsonDeserializer<String> {

    @Override
    public String deserialize(JsonParser parser, DeserializationContext context)
      throws IOException {
        return RegexUtils.removeZeroWidthChars(parser.getText());
    }
}

그런 다음, Integer 필드를 위한 또 다른 버전을 생성합니다:

public class ZeroWidthIntDeserializer extends JsonDeserializer<Integer> {

    @Override
    public Integer deserialize(JsonParser parser, DeserializationContext context)
      throws IOException {
        return Integer.valueOf(RegexUtils.removeZeroWidthChars(parser.getText()));
    }
}

마지막으로, 우리는 이러한 역직렬화기를 Item 필드에 @JsonDeserialize 주석을 사용하여 참조합니다:

@JsonDeserialize(using = ZeroWidthStringDeserializer.class)
private String id;

@JsonDeserialize(using = ZeroWidthStringDeserializer.class)
private String name;

@JsonDeserialize(using = ZeroWidthIntDeserializer.class)
private int value;

4.6. 잘못된 요청 응답 및 스키마

많은 테스트는 우리가 지금까지 한 변경 후에 “잘못된 요청”이라는 결과를 초래할 것입니다. 따라서 보고서에서 경고를 피하기 위해 적절한 @ApiResponse 주석을 추가해야 합니다. 또한, 잘못된 요청에 대한 JSON 응답은 Spring의 BasicErrorController에 의해 동적으로 처리되므로, 클래스 생성하여 주석의 스키마로 사용해야 합니다:

public class BadApiRequest {

    private long timestamp;
    private int status;
    private String error;
    private String path;

    // 기본 getter 및 setter...
}

이제 우리는 컨트롤러에 또 다른 정의를 포함할 수 있습니다:

@ApiResponse(responseCode = "400", description = "Bad Request", content = {
  @Content(
    mediaType = MediaType.APPLICATION_JSON_VALUE, 
    schema = @Schema(implementation = BadApiRequest.class)
  )
})

5. 리팩토링 결과

보고서를 다시 실행하면, 우리의 변경으로 인해 오류가 40% 이상 감소했음을 알 수 있습니다:

리팩토링 결과 해결한 테스트 케이스를 다시 살펴보면, 이제 기본 보안 헤더를 포함하고 있습니다:

CATS 테스트 잘못 형성된 JSON을 거부하고:

CATS 테스트 입력을 정리했습니다:

CATS 테스트 그 결과, 우리는 전체적으로 더 안전한 API를 갖게 되었습니다.

6. 유용한 하위 명령

CATS에는 계약을 검사하고 테스트를 재생하는 데 사용할 수 있는 하위 명령이 있습니다. 흥미로운 두 가지를 살펴보겠습니다.

6.1. API 검사

API 사양에 정의된 모든 경로 및 작업을 나열하려면:

$ cats list --paths -c http://localhost:8080/api-docs

이 명령은 경로별로 그룹화된 결과를 반환합니다:

2 paths and 4 operations:
◼ /api/v1/item: [POST, GET]
◼ /api/v1/item/{id}: [GET, DELETE]

6.2. 테스트 재생

버그 수정 중에 유용한 명령은 replay로, 특정 테스트를 다시 실행합니다:

cats replay Test216

테스트 번호를 확인하고 명령에 교체할 수 있습니다. 각 테스트에 대한 자세한 보고서에는 해당 replay 명령도 포함되어 있어, 이를 복사하여 터미널에 붙여넣을 수 있습니다.

7. 결론

이 기사에서는 CATS를 사용하여 자동화된 OpenAPI 테스트를 수행하는 방법을 탐구했습니다. 이는 수작업의 노력을 상당히 줄이고 테스트 커버리지를 향상시킵니다. 보안 헤더를 추가하고 입력 유효성을 강제하며 엄격한 역직렬화를 구성하는 등의 변경을 적용함으로써, 예시 애플리케이션에서 보고된 오류 수가 40% 이상 감소했습니다.

소스 코드는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다