연습 - 애플리케이션에 단위 테스트 추가

완료됨

이 단원에서는 Microsoft Azure Pipelines로 만든 자동화된 빌드에 단위 테스트를 추가할 예정입니다. 회귀 버그가 팀 코드에 침투하여 순위표의 필터링 기능을 손상시키고 있습니다. 특히, 잘못된 게임 모드가 계속 나타납니다.

다음 이미지는 문제를 보여 줍니다. 사용자가 해당 게임 맵에서 점수만 보여주기 위해 “밀키 웨이”를 선택하는 경우, Andromeda와 같은 다른 게임 맵으로부터 결과를 얻습니다.

A screenshot of the leaderboard showing incorrect results: Andromeda galaxy scores show in the Milky Way galaxy listing.

팀은 오류가 테스터에게 도달하기 전에 오류를 포착하려고 합니다. 단위 테스트는 회귀 버그를 자동으로 테스트하는 좋은 방법입니다.

프로세스의 이 시점에서 단위 테스트를 추가하면 팀이 Space Game 웹앱을 개선하는 데 유리한 출발을 할 수 있습니다. 애플리케이션은 문서 데이터베이스를 사용하여 높은 점수 및 플레이어 프로필을 저장합니다. 지금은 로컬 테스트 데이터를 사용합니다. 나중에는 앱을 라이브 데이터베이스에 연결할 계획입니다.

여러 단위 테스트 프레임워크를 C# 애플리케이션에서 사용할 수 있습니다. 커뮤니티에서 인기가 높기 때문에 NUnit을 사용할 예정입니다.

작업 중인 단위 테스트는 다음과 같습니다.

[TestCase("Milky Way")]
[TestCase("Andromeda")]
[TestCase("Pinwheel")]
[TestCase("NGC 1300")]
[TestCase("Messier 82")]
public void FetchOnlyRequestedGameRegion(string gameRegion)
{
    const int PAGE = 0; // take the first page of results
    const int MAX_RESULTS = 10; // sample up to 10 results

    // Form the query predicate.
    // This expression selects all scores for the provided game region.
    Expression<Func<Score, bool>> queryPredicate = score => (score.GameRegion == gameRegion);

    // Fetch the scores.
    Task<IEnumerable<Score>> scoresTask = _scoreRepository.GetItemsAsync(
        queryPredicate, // the predicate defined above
        score => 1, // we don't care about the order
        PAGE,
        MAX_RESULTS
    );
    IEnumerable<Score> scores = scoresTask.Result;

    // Verify that each score's game region matches the provided game region.
    Assert.That(scores, Is.All.Matches<Score>(score => score.GameRegion == gameRegion));
}

게임 유형과 게임 맵의 조합으로 순위표를 필터링할 수 있습니다.

이 테스트는 높은 점수에 대해 순위표를 쿼리하고, 각 결과가 제공된 게임 맵과 일치하는지 확인합니다.

NUnit 테스트 메서드에서 TestCase는 해당 메서드를 테스트하는 데 사용할 인라인 데이터를 제공합니다. 다음과 같이 NUnit에서 FetchOnlyRequestedGameRegion 단위 테스트 메서드를 호출합니다.

FetchOnlyRequestedGameRegion("Milky Way");
FetchOnlyRequestedGameRegion("Andromeda");
FetchOnlyRequestedGameRegion("Pinwheel");
FetchOnlyRequestedGameRegion("NGC 1300");
FetchOnlyRequestedGameRegion("Messier 82");

Assert.That 메서드에 대한 호출은 테스트 마지막에 나옵니다. 어설션은 true로 선언하는 조건 또는 명령문입니다. 조건이 false로 판명되는 경우 코드에 버그가 있음을 나타낼 수 있습니다. NUnit은 지정한 인라인 데이터를 사용하여 각 테스트 메서드를 실행하고 결과를 통과 또는 실패 테스트로 기록합니다.

많은 단위 테스트 프레임워크는 자연어와 유사한 검증 메서드를 제공합니다. 해당 메서드를 사용하면 테스트를 쉽게 읽고 애플리케이션의 요구 사항에 맞게 테스트를 매핑할 수 있습니다.

다음 예에서 제시된 어설션을 고려해 보세요.

Assert.That(scores, Is.All.Matches<Score>(score => score.GameRegion == gameRegion));

이 줄을 다음과 같이 읽을 수 있습니다.

반환된 각 점수의 게임 영역이 제공된 게임 영역과 일치하는지 어설션합니다.

따라야 할 프로세스는 다음과 같습니다.

  1. 단위 테스트가 포함된 GitHub 리포지토리에서 분기를 페치합니다.
  2. 테스트를 로컬로 실행하여 통과했는지 확인합니다.
  3. 파이프라인 구성에 작업을 추가하여 테스트를 실행하고 결과를 수집합니다.
  4. GitHub 리포지토리에 분기를 푸시합니다.
  5. Azure Pipelines 프로젝트가 자동으로 애플리케이션을 빌드하고 테스트를 실행하는 것을 확인합니다.

GitHub에서 분기 가져오기

여기서는 GitHub에서 unit-tests 분기를 페치하거나, 체크 아웃하거나, 이 분기로 전환합니다.

이 분기에는 이전 모듈에서 작업한 Space Game 프로젝트와 새로 시작할 Azure Pipelines 구성이 포함되어 있습니다.

  1. Visual Studio Code에서 통합 터미널을 엽니다.

  2. 다음과 같이 git 명령을 실행하여 Microsoft 리포지토리에서 unit-tests라는 분기를 페치한 다음 해당 분기로 전환합니다.

    git fetch upstream unit-tests
    git checkout -B unit-tests upstream/unit-tests
    

    이 명령 형식을 사용하면 upstream이라고 하는 Microsoft GitHub 리포지토리에서 시작 코드를 가져올 수 있습니다. 잠시 후에 해당 분기를 origin이라고 하는 GitHub 리포지토리로 푸시합니다.

  3. 선택적인 단계로 Visual Studio Code에서 azure-pipelines.yml 파일을 열고 초기 구성을 숙지합니다. 이 구성은 Azure Pipelines를 사용하여 빌드 파이프라인 만들기 모듈에서 만든 기본 구성과 비슷합니다. 이 구성은 애플리케이션의 릴리스 구성만 빌드합니다.

로컬로 테스트 실행

항상 파이프라인에 테스트를 제출하기 전에 로컬로 테스트를 실행하는 것이 좋습니다. 여기서 그렇게 할 수 있습니다.

  1. Visual Studio Code에서 통합 터미널을 엽니다.

  2. dotnet build를 실행하여 솔루션의 각 프로젝트를 빌드합니다.

    dotnet build --configuration Release
    
  3. 다음과 같이 dotnet test 명령을 실행하여 단위 테스트를 실행합니다.

    dotnet test --configuration Release --no-build
    

    --no-build 플래그는 프로젝트를 실행하기 전에 빌드하지 않도록 지정합니다. 이전 단계에서 프로젝트를 빌드했으므로 빌드할 필요가 없습니다.

    다섯 가지 테스트가 모두 통과된 것을 확인해야 합니다.

    Starting test execution, please wait...
    A total of 1 test files matched the specified pattern.
    
    Passed!  - Failed:     0, Passed:     5, Skipped:     0, Total:     5, Duration: 57 ms
    

    이 예제에서는 테스트가 실행되는 데 1초 미만이 걸렸습니다.

    테스트가 5개 있었다는 점을 기억하세요. 테스트 메서드 FetchOnlyRequestedGameRegion 하나만 정의하지만, 해당 테스트는 TestCase 인라인 데이터에 지정된 대로 각 게임 맵에 대해 한 번씩 5번 실행됩니다.

  4. 테스트를 두 번째로 실행합니다. 이번에는 결과를 로그 파일에 쓸 수 있도록 --logger 옵션을 제공합니다.

    dotnet test Tailspin.SpaceGame.Web.Tests --configuration Release --no-build --logger trx
    

    출력에서 TestResults 디렉터리에 TRX 파일이 생성되었음을 알 수 있습니다.

    TRX 파일은 테스트 실행 결과를 포함하는 XML 문서입니다. Visual Studio와 다른 도구는 결과를 시각화하는 데 유용하기 때문에 NUnit 테스트에서 널리 사용되는 형식입니다.

    나중에 Azure Pipelines가 파이프라인을 통해 실행될 때 테스트 결과를 시각화하고 추적하는 데 어떻게 도움이 되는지 확인합니다.

    참고 항목

    TRX 파일은 소스 제어에 포함되지 않습니다. .gitignore 파일을 사용하면 Git이 무시할 임시 파일과 기타 파일을 지정할 수 있습니다. 프로젝트의 .gitignore 파일이 이미 TestResults 디렉터리에 있는 모든 항목을 무시하도록 설정되어 있습니다.

  5. 선택적 단계로, Visual Studio Code에서 Tailspin.SpaceGame.Web.Tests 폴더의 DocumentDBRepository_GetItemsAsyncShould.cs 파일을 열고 테스트 코드를 검사합니다. 특별히 .NET 앱을 빌드하는 데 관심이 없더라도 이 테스트 코드는 다른 단위 테스트 프레임워크에서 볼 수 있는 코드와 비슷하므로 유용합니다.

파이프라인 구성에 작업 추가

빌드 파이프라인을 구성하여 단위 테스트를 실행하고 결과를 수집합니다.

  1. Visual Studio Code에서 azure-pipelines.yml을 다음과 같이 수정합니다.

    trigger:
    - '*'
    
    pool:
      vmImage: 'ubuntu-20.04'
      demands:
      - npm
    
    variables:
      buildConfiguration: 'Release'
      wwwrootDir: 'Tailspin.SpaceGame.Web/wwwroot'
      dotnetSdkVersion: '6.x'
    
    steps:
    - task: UseDotNet@2
      displayName: 'Use .NET SDK $(dotnetSdkVersion)'
      inputs:
        version: '$(dotnetSdkVersion)'
    
    - task: Npm@1
      displayName: 'Run npm install'
      inputs:
        verbose: false
    
    - script: './node_modules/.bin/node-sass $(wwwrootDir) --output $(wwwrootDir)'
      displayName: 'Compile Sass assets'
    
    - task: gulp@1
      displayName: 'Run gulp tasks'
    
    - script: 'echo "$(Build.DefinitionName), $(Build.BuildId), $(Build.BuildNumber)" > buildinfo.txt'
      displayName: 'Write build info'
      workingDirectory: $(wwwrootDir)
    
    - task: DotNetCoreCLI@2
      displayName: 'Restore project dependencies'
      inputs:
        command: 'restore'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Build the project - $(buildConfiguration)'
      inputs:
        command: 'build'
        arguments: '--no-restore --configuration $(buildConfiguration)'
        projects: '**/*.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: true
        projects: '**/*.Tests.csproj'
    
    - task: DotNetCoreCLI@2
      displayName: 'Publish the project - $(buildConfiguration)'
      inputs:
        command: 'publish'
        projects: '**/*.csproj'
        publishWebProjects: false
        arguments: '--no-build --configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/$(buildConfiguration)'
        zipAfterPublish: true
    
    - task: PublishBuildArtifacts@1
      displayName: 'Publish Artifact: drop'
      condition: succeeded()
    

    이 버전에서는 이 DotNetCoreCLI@2 빌드 작업이 도입됩니다.

    - task: DotNetCoreCLI@2
      displayName: 'Run unit tests - $(buildConfiguration)'
      inputs:
        command: 'test'
        arguments: '--no-build --configuration $(buildConfiguration)'
        publishTestResults: true
        projects: '**/*.Tests.csproj'
    

    이 빌드 작업에서는 dotnet test 명령이 실행됩니다.

    이 작업은 테스트를 수동으로 실행했을 때 사용한 --logger trx 인수를 지정하지 않습니다. publishTestResults 인수가 추가됩니다. 이 인수는 $(Agent.TempDirectory) 기본 제공 변수를 통해 액세스할 수 있는 임시 디렉터리에 대한 TRX 파일을 생성하도록 파이프라인에 지시합니다. 또한 작업 결과를 파이프라인에 게시합니다.

    projects 인수는 "**/*.Tests.csproj"와 일치하는 모든 C# 프로젝트를 지정합니다. "**" 부분은 모든 디렉터리와 일치하며, "*.Tests.csproj" 파트는 파일 이름이 ".Tests.csproj"로 끝나는 모든 프로젝트와 일치합니다. unit-tests 분기에는 Tailspin.SpaceGame.Web.Tests.csproj라는 단위 테스트 프로젝트만 포함됩니다. 패턴을 지정하면 빌드 구성을 수정할 필요 없이 더 많은 테스트 프로젝트를 실행할 수 있습니다.

GitHub에 분기 푸시

여기서는 변경 내용을 GitHub로 푸시하고 파이프라인 실행을 확인합니다. 다시 말하지만, 현재 unit-tests 분기에 있습니다.

  1. 통합 터미널에서 azure-pipelines.yml을 인덱스에 추가하고, 변경 내용을 커밋하며, 분기를 GitHub로 푸시합니다.

    git add azure-pipelines.yml
    git commit -m "Run and publish unit tests"
    git push origin unit-tests
    

테스트를 실행하는 Azure Pipelines 보기

여기서는 테스트가 파이프라인에서 실행되는 것을 확인한 다음 Microsoft Azure Test Plans에서 결과를 시각화합니다. Azure Test Plans는 애플리케이션을 성공적으로 테스트하는 데 필요한 모든 도구를 제공합니다. 수동 테스트 계획을 만들고 실행하며, 자동화된 테스트를 생성하고, 관련자의 피드백을 수집할 수 있습니다.

  1. Azure Pipelines에서 각 단계를 통해 빌드를 추적합니다.

    사용자가 명령줄에서 수동으로 실행한 것처럼 단위 테스트 실행 - 릴리스 작업이 단위 테스트를 실행하는 것을 확인합니다.

    A screenshot of Azure Pipelines showing console output from running unit tests.

  2. 파이프라인 요약으로 다시 이동합니다.

  3. 테스트 탭으로 이동합니다.

    테스트 실행의 요약이 표시됩니다. 5개 테스트가 모두 통과되었습니다.

    A screenshot of Azure Pipelines showing the Tests tab with 5 total tests run and 100 percent passing.

  4. Azure DevOps에서 Test Plans를 선택한 다음, Runs를 선택합니다.

    A screenshot of Azure DevOps navigation menu with Test Plans section and Runs tab highlighted.

    방금 실행한 것을 포함하여 가장 최근 테스트가 실행됨을 확인합니다.

  5. 가장 최근의 테스트 실행을 두 번 클릭합니다.

    결과의 요약이 표시됩니다.

    A screenshot of Azure DevOps test run results summary showing 5 passed tests.

    이 예제에서 5개 테스트가 모두 통과되었습니다. 테스트가 실패하는 경우 추가 세부 정보를 가져오려면 빌드 작업으로 이동할 수 있습니다.

    또한 Visual Studio 또는 다른 시각화 도구를 통해 검사할 수 있도록 TRX 파일을 다운로드할 수 있습니다.

테스트를 하나만 추가했지만 이는 좋은 시작이며 즉각적인 문제를 해결합니다. 이제 팀에서 더 많은 테스트를 추가하고 실행하면서 프로세스를 개선할 수 있는 기반이 마련되었습니다.

기본으로 분기 병합

실제 시나리오에서 결과가 만족스러우면 unit-tests 분기를 main에 병합할 수 있지만 간결성을 위해 지금은 해당 프로세스를 건너뛰겠습니다.