속성 기반 테스트와 클린 아키텍처는 완벽하게 맞습니다.

clean architecture에 대해 많이 쓰여졌습니다. 주요 가치는 무거운 모의를 활용하지 않고 핵심 비즈니스 로직을 테스트할 수 있게 해주는 부작용 도메인 계층이 없는 유지 관리 기능입니다.

그러나 순수한 도메인 로직에 대한 테스트를 설계할 때 우리는 그렇게 까다로운 경향이 없습니다. 단위 테스트에는 overspecified software 과 같은 많은 함정이 포함되어 있습니다. 그러나 매우 간단한 프로세스로 보일 수 있는 순수한 기능을 테스트하는 경우에도 몇 가지 함정에 직면할 수 있습니다.

그 중 하나는 단위 테스트를 작성할 때 일종의 임의의 매직 넘버에 의존한다는 것입니다. 함수가 주어진 지점에서 올바르게 작동한다고 보장할 수 있지만 모든 지점에서 작동한다고 보장할 수는 없습니다. 대안은 함수가 일부 기준을 지속적으로 충족하는지 여부를 확인하는 것입니다.

그리고 이것이 속성 기반 테스트가 목표로 하는 것입니다. 하드코딩된 입력 지점에서 출력을 확인하는 대신 생성된 여러 값으로 정의한 함수의 속성을 확인합니다.

어떻게 작동하는지 알아보기 위해 코드 예제를 살펴보겠습니다. 아래는 내 프로젝트Kyiv Station Walk의 예입니다. 도메인에서 체크포인트 모음을 가져와 프레젠테이션 계층의 규칙에 맞게 변환하는 기능을 볼 수 있습니다.

let removeRedundantCheckpoints (checkPoints : Location[]) =
    let checkPointsMaxCount = 5
    let isStartOrEndOfTheRoute (checkPoints : Location[]) i =
       i = 0 || i = checkPoints.Length - 1
    let euclidianDistance c1 c2 =
        Math.Pow(float(c1.lattitude - c2.lattitude), float(2)) + Math.Pow(float(c1.longitude - c2.longitude), float(2))
    if checkPoints.Length <= 5 then
        checkPoints
    else
        checkPoints
        |> Array.mapi(fun i c ->
            if isStartOrEndOfTheRoute checkPoints i then
                {
                    index = i
                    checkPoint = c
                    distanceToNextCheckPoint = float(1000000)
                }
            else
                {
                    index = i
                    checkPoint = c
                    distanceToNextCheckPoint = euclidianDistance checkPoints.[i+1] c
                }
        )
        |> Array.sortByDescending(fun i -> i.distanceToNextCheckPoint)
        |> Array.take(checkPointsMaxCount)
        |> Array.sortBy(fun i -> i.index)
        |> Array.map(fun i -> i.checkPoint)


임의의 체크포인트 배열을 제공하고 출력을 확인하거나 대신 함수가 충족해야 하는 일부 속성에 대해 생각할 수 있습니다. 다음은 코드로 표현된 이러한 속성입니다.

open FsCheck.Xunit
open RouteModels

module RemoveRedundantCheckpointsTests =

    let ``result array contains no more than 5 items`` input mapFn =
        let res = mapFn input
        Array.length res <= 5

    [<Property>]
    let maxLength x =
        ``result array contains no more than 5 items`` x removeRedundantCheckpoints

    let ``result contains first point from input`` (input: Location[]) (mapFn : Location[] -> Location[]) =
        if Array.length input = 0 then
            true
        else
            let res = mapFn input
            res.[0] = input.[0]

    [<Property>]
    let firstItem x =
        ``result contains first point from input`` x removeRedundantCheckpoints

    let ``result contains last point from input`` (input: Location[]) (mapFn : Location[] -> Location[]) =
        if Array.length input = 0 then
            true
        else
            let res = mapFn input
            res.[res.Length-1] = input.[input.Length-1]

    [<Property>]
    let lastItem x =
        ``result contains last point from input`` x removeRedundantCheckpoints

    let ``result contains only points from input`` input mapFn =
        let res = mapFn input
        Array.length (Array.except input res) = 0

    [<Property>]
    let onlyInput x =
        ``result contains only points from input`` x removeRedundantCheckpoints


imports 문에서 볼 수 있듯이 우리는 FsCheck에 의존하여 임의의 값을 생성합니다.

코드 뒷부분에서 매퍼 함수와 입력 배열을 받아들이고 속성이 충족되는지 확인하는 부울 조건을 반환하는 고차 함수를 선언합니다. 이중 백틱은 속성을 자연어로 표현할 수 있는 편리한 F# 기능입니다.

이 테스트는 Property 속성으로 장식되어 있으며 변경될 수 있는 removeRedundantCheckpoints 기능뿐만 아니라 FsCheck에서 생성된 입력을 허용합니다. 이러한 설정을 통해 테스트 중인 함수가 라이브러리에서 생성된 다수의 임의 값으로 제공된 속성을 충족하는지 여부를 확인할 수 있습니다.

결론



테스트와 관련하여 많은 팀이 애플리케이션 코드와 마찬가지로 테스트 도구 모음을 설계하는 데 동일한 노력을 기울입니다. 그리고 전통적인 테스트 피라미드 이외의 것을 거의 고려하지 않는 사람들도 있습니다. 여전히 속성 기반 테스트는 일반적으로 애플리케이션의 도메인 계층 또는 매핑 계층에 있는 순수 논리에 대한 좋은 옵션을 나타냅니다.

좋은 웹페이지 즐겨찾기