UI 테스트 작성

완료됨

이 섹션에서는 Amita가 설명한 UI 동작을 확인하는 Selenium 테스트를 작성하도록 Andy와 Amita를 돕습니다.

Amita는 일반적으로 Chrome, Firefox, Microsoft Edge에서 테스트를 실행합니다. 여기서도 동일한 작업을 수행합니다. 사용할 Microsoft 호스팅 에이전트는 이러한 해당하는 각 브라우저에서 작동하도록 미리 구성되어 있습니다.

GitHub에서 분기 가져오기

이 섹션에서는 GitHub에서 selenium 분기를 페치합니다. 그런 다음 해당 분기를 ‘체크 아웃’하거나 해당 분기로 전환합니다. 분기의 콘텐츠는 Andy, Amita가 작성하는 테스트를 따르는 데 도움이 됩니다.

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

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

  2. Microsoft 리포지토리에서 selenium이라는 분기를 다운로드하고 해당 분기로 전환하려면 git fetchgit checkout 명령을 실행합니다.

    git fetch upstream selenium
    git checkout -B selenium upstream/selenium
    

    이전 단원에서 Amita의 수동 테스트를 수행했다면 이러한 명령을 이미 실행했을 수 있습니다. 이전 단원에서 이미 실행한 경우 지금 다시 실행할 수 있습니다.

    upstream은 Microsoft GitHub 리포지토리를 나타냅니다. 프로젝트의 Git 구성은 업스트림 원격을 이해하는데, 이는 해당 관계를 이미 설정했기 때문입니다. 해당 관계는 Microsoft 리포지토리에서 프로젝트를 포크하여 로컬에서 복제했을 때 설정했습니다.

    잠시 후에 이 분기를 origin이라는 GitHub 리포지토리로 푸시합니다.

  3. 필요에 따라 Visual Studio Code에서 azure-pipelines.yml 파일을 엽니다. 초기 구성에 익숙해져야 합니다.

    구성은 이 학습 경로의 이전 모듈에서 만든 구성과 유사합니다. 이 구성은 애플리케이션의 릴리스 구성만 빌드합니다. 간단히 하기 위해 이전 모듈에서 설정한 트리거, 수동 승인, 테스트가 생략됩니다.

    참고

    더 강력한 구성은 빌드 프로세스에 참여하는 분기를 지정할 수 있습니다. 예를 들어 코드 품질을 확인하기 위해 모든 분기에 변경 내용을 푸시할 때마다 단위 테스트를 실행할 수 있습니다. 더 철저한 테스트를 수행하는 환경에 애플리케이션을 배포할 수도 있습니다. 하지만 끌어오기 요청이 있거나, 릴리스 후보가 있거나, 코드를 ‘main’에 병합할 때만 이 배포를 실행합니다.

    자세한 내용은 Git 및 GitHub를 사용하여 빌드 파이프라인에서 코드 워크플로 구현빌드 파이프라인 트리거를 참조하세요.

단위 테스트 코드 작성

Amita는 웹 브라우저를 제어하는 코드를 작성하는 방법을 열심히 배우고 있습니다.

Amita와 Andy가 함께 Selenium 테스트를 작성할 것입니다. Andy가 이미 빈 NUnit 프로젝트를 설정했습니다. 프로세스를 진행하면서 Amita와 Andy는 Selenium 설명서, 몇 가지 온라인 자습서와 Amita가 테스트를 수동으로 수행했을 때 적어둔 메모를 참조할 것입니다. 이 모듈을 마치면 프로세스 진행에 도움이 될 추가 리소스를 알게 될 것입니다.

Andy와 Amita가 테스트를 작성하는 데 사용하는 프로세스를 검토해 보겠습니다. Visual Studio Code의 Tailspin.SpaceGame.Web.UITests 디렉터리에서 HomePageTest.cs를 열어 따라갑니다.

HomePageTest 클래스 정의

Andy: 가장 먼저 할 일은 테스트 클래스를 정의하는 것입니다. 여러 명명 규칙 중 하나를 따르도록 선택할 수 있습니다. 클래스 HomePageTest를 호출해 보겠습니다. 이 클래스에는 홈페이지와 관련된 모든 테스트를 저장합니다.

Andy는 다음 코드를 HomePageTest에 추가합니다.

public class HomePageTest
{
}

Andy: NUnit 프레임워크에서 사용할 수 있도록 이 클래스를 public으로 표시해야 합니다.

IWebDriver 멤버 변수 추가

Andy: 다음에는 IWebDriver 멤버 변수가 필요합니다. IWebDriver는 웹 브라우저를 시작하고 웹페이지 콘텐츠와 상호 작용하는데 사용하는 프로그래밍 인터페이스입니다.

Amita: 프로그래밍의 인터페이스에 대해 들어본 적이 있어요. 자세히 설명해주시겠어요?

Andy: 인터페이스는 구성 요소가 어떻게 동작해야 하는지에 대한 사양 또는 청사진이라고 생각하면 됩니다. 인터페이스는 구성 요소에 메서드 또는 동작을 제공하지만 기본 세부 정보는 제공하지 않습니다. Amita 아니면 다른 사람이 인터페이스를 구현하는 “구체적 클래스”를 하나 이상 만들 수 있어요. Selenium은 필요한 구체적인 클래스를 제공합니다.

다음 다이어그램은 IWebDriver 인터페이스와 해당 인터페이스를 구현하는 몇 가지 클래스를 보여 줍니다.

Diagram of the IWebDriver interface, its methods, and concrete classes.

다이어그램에는 IWebDriver가 제공하는 3가지 메서드 Navigate, FindElement, Close가 있습니다.

여기에 표시된 3가지 클래스인 ChromeDriver, FirefoxDriver, EdgeDriver는 각각 IWebDriver와 그 메서드를 구현합니다. SafariDriver 등과 같은 다른 클래스도 있는데, 역시 IWebDriver를 구현합니다. 각 드라이버 클래스는 자신이 나타내는 웹 브라우저를 제어할 수 있습니다.

Andy는 다음 코드와 같이 멤버 변수 driverHomePageTest 클래스에 추가합니다.

public class HomePageTest
{
    private IWebDriver driver;
}

테스트 픽스쳐 정의

Andy: Chrome, Firefox, Edge에서 전체 테스트 세트를 실행하려고 합니다. NUnit에서 테스트 픽스쳐를 사용하여 테스트하려는 각 브라우저에서 전체 테스트 세트를 한 번씩, 여러 번 실행할 수 있습니다.

NUnit에서 TestFixture 특성을 사용하여 테스트 픽스쳐를 정의합니다. Andy는 다음 세 가지 테스트 픽스쳐를 HomePageTest 클래스에 추가합니다.

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private IWebDriver driver;
}

Andy: 다음에는 테스트 클래스의 생성자를 정의해야 합니다. 생성자는 NUnit이 해당 클래스의 인스턴스를 만들 때 호출됩니다. 생성자는 클래스의 인수로 테스트 픽스쳐에 연결한 문자열을 사용합니다. 코드는 다음과 같습니다.

[TestFixture("Chrome")]
[TestFixture("Firefox")]
[TestFixture("Edge")]
public class HomePageTest
{
    private string browser;
    private IWebDriver driver;

    public HomePageTest(string browser)
    {
        this.browser = browser;
    }
}

Andy:browser 멤버 변수를 추가하여 설정 코드에서 현재 브라우저 이름을 사용할 수 있습니다. 다음에 설정 코드를 작성해 보겠습니다.

설정 메서드 정의

Andy: 다음으로 테스트 중인 브라우저에 대해 해당 인터페이스를 구현하는 클래스 인스턴스에 IWebDriver 멤버 변수를 할당해야 합니다. ChromeDriver, FirefoxDriver, EdgeDriver 클래스는 각각 Chrome, Firefox, Edge에 대해 인터페이스를 구현합니다.

driver 변수를 설정하는 메서드 Setup을 만들어 보겠습니다. OneTimeSetUp 특성을 사용하여 이 메서드를 테스트 픽스쳐마다 한 번씩 실행하도록 NUnit에 지시합니다.

[OneTimeSetUp]
public void Setup()
{
}

Setup 메서드에서는 switch 문을 사용하여 브라우저 이름에 따라 적절한 구체적인 구현에 driver 멤버 변수를 할당할 수 있습니다. 지금 해당 코드를 추가해 보겠습니다.

// Create the driver for the current browser.
switch(browser)
{
    case "Chrome":
    driver = new ChromeDriver(
        Environment.GetEnvironmentVariable("ChromeWebDriver")
    );
    break;
    case "Firefox":
    driver = new FirefoxDriver(
        Environment.GetEnvironmentVariable("GeckoWebDriver")
    );
    break;
    case "Edge":
    driver = new EdgeDriver(
        Environment.GetEnvironmentVariable("EdgeWebDriver"),
        new EdgeOptions
        {
            UseChromium = true
        }
    );
    break;
    default:
    throw new ArgumentException($"'{browser}': Unknown browser");
}

각 드라이버 클래스의 생성자는 Selenium에서 웹 브라우저를 제어하는 데 필요한 드라이버 소프트웨어에 대한 선택적 경로를 사용합니다. 여기에 표시된 환경 변수의 역할에 대해서는 나중에 설명합니다.

이 예제에서 EdgeDriver 생성자에는 Edge Chromium 버전을 사용한다고 지정하는 추가 옵션이 필요합니다.

도우미 메서드 정의

Andy: 테스트 전체에서 두 작업을 반복해야 합니다.

  • 페이지에서 요소 찾기(예: 클릭한 링크 및 표시될 것으로 예상되는 모달 창).
  • 페이지의 클릭 요소(예: 모달 창을 표시하는 링크, 각 모달을 닫는 단추).

각 작업에 대해 하나씩, 도우미 메서드를 두 개 작성하겠습니다. 페이지에서 요소를 찾는 메서드로 시작합니다.

FindElement 도우미 메서드 작성

페이지에서 요소를 찾을 때 일반적으로 페이지 로드 또는 사용자의 정보 입력과 같은 다른 이벤트에 대한 응답으로 발생합니다. Selenium은 조건이 true가 될 때까지 대기할 수 있는 WebDriverWait 클래스를 제공합니다. 지정된 기간 내에 조건이 참이 아니면 WebDriverWait에서는 예외 또는 오류를 throw합니다. WebDriverWait 클래스를 사용하여 지정된 요소가 표시되고 사용자 입력값을 받을 준비가 될 때까지 대기할 수 있습니다.

페이지에서 요소를 찾으려면 By 클래스를 사용합니다. By 클래스는 이름, CSS 클래스 이름, HTML 태그 또는 이 경우에는 id 특성을 사용하여 요소를 찾을 수 있는 메서드를 제공합니다.

Andy와 Amita는 FindElement 도우미 메서드를 코딩합니다. 다음 코드와 같이 표시됩니다.

private IWebElement FindElement(By locator, IWebElement parent = null, int timeoutSeconds = 10)
{
    // WebDriverWait enables us to wait for the specified condition to be true
    // within a given time period.
    return new WebDriverWait(driver, TimeSpan.FromSeconds(timeoutSeconds))
        .Until(c => {
            IWebElement element = null;
            // If a parent was provided, find its child element.
            if (parent != null)
            {
                element = parent.FindElement(locator);
            }
            // Otherwise, locate the element from the root of the DOM.
            else
            {
                element = driver.FindElement(locator);
            }
            // Return true after the element is displayed and is able to receive user input.
            return (element != null && element.Displayed && element.Enabled) ? element : null;
        });
}

ClickElement 도우미 메서드 작성

Andy: 다음으로 링크를 클릭하는 도우미 메서드를 작성하겠습니다. Selenium에서는 이 메서드를 작성하는 몇 가지 방법을 제공합니다. 그중 하나는 IJavaScriptExecutor 인터페이스입니다. 해당 인터페이스와 함께 JavaScript를 사용하여 프로그래밍 방식으로 링크를 클릭할 수 있습니다. 이 접근 방식은 먼저 뷰로 스크롤하지 않아도 링크를 클릭할 수 있음으로 잘 작동합니다.

ChromeDriver, FirefoxDriver, EdgeDriver 각각이 IJavaScriptExecutor를 구현합니다. 드라이버를 이 인터페이스로 캐스트한 다음 ExecuteScript를 호출하여 기본 HTML 개체에서 JavaScript click() 메서드를 실행해야 합니다.

Andy와 Amita는 ClickElement 도우미 메서드를 코딩합니다. 다음 코드와 같이 표시됩니다.

private void ClickElement(IWebElement element)
{
    // We expect the driver to implement IJavaScriptExecutor.
    // IJavaScriptExecutor enables us to execute JavaScript code during the tests.
    IJavaScriptExecutor js = driver as IJavaScriptExecutor;

    // Through JavaScript, run the click() method on the underlying HTML object.
    js.ExecuteScript("arguments[0].click();", element);
}

Amita: 이와 같은 도우미 메서드를 추가한다는 아이디어가 참 좋네요. 거의 모든 테스트에서 사용할 수 있을 정도로 일반적으로 보입니다. 나중에 필요에 따라 도우미 메서드를 더 추가할 수 있습니다.

테스트 메서드 정의

Andy: 이제 테스트 메서드를 정의할 준비가 되었습니다. 이전에 실행한 수동 테스트를 기반으로 이 메서드를 ClickLinkById_ShouldDisplayModalById라고 하겠습니다. 테스트에서 수행하는 작업을 정확하게 정의하는, 설명이 포함된 이름을 테스트 메서드에 지정하는 것이 좋습니다. 여기에서 id 특성으로 정의된 링크를 클릭합니다. 또한 id 특성을 사용하여 적절한 모달 창이 표시되는지 확인하려고 합니다.

Andy는 테스트 메서드의 시작 코드를 추가합니다.

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
}

Andy: 코드를 더 추가하기 전에 이 테스트에서 수행해야 하는 작업을 정의해 볼게요.

Amita: 이 부분은 제가 처리할 수 있어요. 다음 작업을 수행하려고 합니다.

  1. id 특성으로 링크를 찾은 다음 해당 링크를 클릭합니다.
  2. 결과 모달을 찾습니다.
  3. 모달을 닫습니다.
  4. 모달이 성공적으로 표시되었는지 확인합니다.

Andy: 맞아요. 또한 몇 가지 다른 작업도 처리해야 합니다. 예를 들어 드라이버를 로드할 수 없는 경우 테스트를 무시해야 하며, 모달이 성공적으로 표시된 경우에만 모달을 닫아야 합니다.

머그잔에 커피를 다시 채운 후 Andy와 Amita는 테스트 메서드에 코드를 추가합니다. 직접 작성한 도우미 메서드를 사용하여 페이지 요소를 찾고 링크 및 단추를 클릭합니다. 결과:

public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{
    // Skip the test if the driver could not be loaded.
    // This happens when the underlying browser is not installed.
    if (driver == null)
    {
        Assert.Ignore();
        return;
    }

    // Locate the link by its ID and then click the link.
    ClickElement(FindElement(By.Id(linkId)));

    // Locate the resulting modal.
    IWebElement modal = FindElement(By.Id(modalId));

    // Record whether the modal was successfully displayed.
    bool modalWasDisplayed = (modal != null && modal.Displayed);

    // Close the modal if it was displayed.
    if (modalWasDisplayed)
    {
        // Click the close button that's part of the modal.
        ClickElement(FindElement(By.ClassName("close"), modal));

        // Wait for the modal to close and for the main page to again be clickable.
        FindElement(By.TagName("body"));
    }

    // Assert that the modal was displayed successfully.
    // If it wasn't, this test will be recorded as failed.
    Assert.That(modalWasDisplayed, Is.True);
}

Amita: 지금까지 코딩에는 전혀 문제가 없어 보이네요. 그런데 앞서 수집한 id 특성에 테스트를 어떻게 연결하죠?

Andy: 좋은 질문입니다. 바로 다음에 해당 문제를 다루려고 합니다.

테스트 사례 데이터 정의

Andy: NUnit에서는 몇 가지 방법으로 테스트에 데이터를 제공할 수 있습니다. 여기서는 TestCase 특성을 사용합니다. 해당 특성은 실행될 때 테스트 메서드에 다시 전달되는 인수를 사용합니다. 각각 애플리케이션의 다른 기능을 테스트하는 여러 TestCase 특성을 가질 수 있습니다. 각 TestCase 특성은 파이프라인 실행이 끝날 때 표시되는 보고서에 포함되는 테스트 사례를 생성합니다.

Andy는 해당 TestCase 특성을 테스트 메서드에 추가합니다. 다음 특성은 Download game(게임 다운로드) 단추, 게임 화면 중 하나, 순위표의 1위 플레이어를 나타냅니다. 각 특성은 id 특성 두 개를 지정하는데, 하나는 클릭할 링크의 특성이고 다른 하나는 해당하는 모달 창의 특성입니다.

// Download game
[TestCase("download-btn", "pretend-modal")]
// Screen image
[TestCase("screen-01", "screen-modal")]
// // Top player on the leaderboard
[TestCase("profile-1", "profile-modal-1")]
public void ClickLinkById_ShouldDisplayModalById(string linkId, string modalId)
{

...

Andy:TestCase 특성의 첫 번째 매개 변수는 클릭할 링크의 id 특성입니다. 두 번째 매개 변수는 표시되어야 하는 모달 창의 id 특성입니다. 해당 매개 변수가 테스트 메서드의 문자열 인수 두 개에 해당하는 방식이 보일 겁니다.

Amita: 네 보이네요. 약간 더 연습하면 고유한 테스트를 추가할 수 있을 것 같아요. 우리 파이프라인에서는 언제 해당 테스트가 실행되는 것을 볼 수 있을까요?

Andy: 파이프라인 전체의 변경 내용을 푸시하기 전에 먼저 코드가 컴파일되어 로컬로 실행되는지 확인해 보죠. 모두 제대로 작동하는지 확인한 후에만 GitHub에 대한 변경 내용을 커밋 후 푸시하고 파이프라인에서 해당 변경 내용이 적용되는 것을 볼 수 있습니다. 지금 테스트를 로컬로 실행해 보겠습니다.