Blazor를 사용한 소프트웨어 설문 조사 - 섹션 8-bUnit

15917 단어
나는 Blazor로 작은 조사 사이트를 쓰기로 결정했다.이 부분은 Blazor를 배우는 핑계다.
Blazor에 대한 이해에 따라, 나는 일련의 문장 형식으로 블로그를 발표할 것이다.
이 시리즈는 Blazor를 위한 교육 과정이 아니라 이 제품을 사용할 때의 사고 과정을 배우고 있습니다.
이 시리즈의 초기 기사:
  • Part 1 - State
  • Part 2 - Better State
  • Part 3 - Components
  • Part 4 - Render Mode
  • Part 5 - Client IP
  • Part 6 - Azure Cosmos DB
  • Part 7 - Azure SignalR Service
  • 현대 발전에서 단원 테스트 능력이 없다면 어떤 신기술도 흡인력을 얻기 어렵다.
    Blazor에게 이것이 바로 bUnit입니다.
    나는 마이크로소프트에 자신의 Blazor 도구가 없다는 것을 발견했을 때 나는 약간 놀랐다. 반대로 그들은 사실상 제3자 소스 공급 도구를 홍보하고 있었다.

    "There's no official Microsoft testing framework for Blazor, but the community-driven project bUnit provides a convenient way to unit test Blazor components." Microsoft Docs


    비록 나는 마이크로소프트가 개원 커뮤니티를 포옹하는 것을 정말 좋아하지만, 그들이 자신의 제품이 없는 것은 확실히 좀 이상하다.
    그럼에도 불구하고, bUnit는 매우 완벽한 개발과 유지보수를 위한 도구인 것 같다.
    나는 당연히 시작하기 전에 먼저 bUnit documentation site을 통독할 것을 건의한다.문서가 아주 좋아서 운행이 매우 빠르다.

    요약


    따라서 bUnit는 xUnit, NUnit 또는 MSTest와 합작하여 Blazor 구성 요소 테스트에 많은 도움말 도구를 제공하였다.
    나는 bUnit가 xUnit을 좋아한다고 믿는다. 이것은 나에게 매우 좋다. 왜냐하면 xUnit는 나의dotnet 단원 테스트 프레임워크이기 때문이다.
    bUnit에서는 세 가지 방법으로 테스트를 작성할 수 있습니다.
  • C#style
  • 면도기 고정장치
  • 면도기 스냅샷
  • C# 스타일


    이것은 상당히 전통적인 스타일로 xUnit 경험이 있는 사람은 누구나 그것을 잘 식별할 수 있어야 한다(이하 bUnit Docs).
    using Xunit;
    using Bunit;
    
    namespace Bunit.Docs.Samples
    {
      public class HelloWorldTest
      {
        [Fact]
        public void HelloWorldComponentRendersCorrectly()
        {
          // Arrange
          using var ctx = new TestContext();
    
          // Act
          var cut = ctx.RenderComponent<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    }
    
    이곳의 관건은TestContext이다. 이것은 bUnit의 일부이며, Blazor 구성 요소와 상호작용에 필요한 모든 마력을 제공한다.

    면도기 고정 장치


    이 테스트는 Razor 구문을 사용하여 작성된 방식과는 다릅니다.그런 다음 고정장치 테스트를 통해 C# 스타일과 유사한 단언(샘플은 bUnit Documentation Page)을 수행할 수 있습니다.
    @inherits TestComponentBase
    
    <Fixture Test="HelloWorldComponentRendersCorrectly">
      <ComponentUnderTest>
        <HelloWorld />
      </ComponentUnderTest>
    
      @code
      {
        void HelloWorldComponentRendersCorrectly(Fixture fixture)
        {
          // Act
          var cut = fixture.GetComponentUnderTest<HelloWorld>();
    
          // Assert
          cut.MarkupMatches("<h1>Hello world from Blazor</h1>");
        }
      }
    </Fixture>
    
    여기서 가장 큰 차이점은 설치가 Razor 구문을 통해 이루어졌다는 것입니다.
    개인적으로 C# 스타일을 좋아합니다. 아직 면도기로 고정장치 스타일을 사용하는 이유를 찾지 못했습니다.나는 이것이 주로 개인의 품격의 문제라고 의심한다.

    Blazor 스냅샷


    이 테스트도 Razor 구문을 사용하여 작성되었으나 일반적인 선언이 아닌 예상 출력을 지정했습니다(샘플은 bUnit Documentation Page).
    @inherits TestComponentBase
    
    <SnapshotTest Description="HelloWorld component renders correctly">
      <TestInput>
        <HelloWorld />
      </TestInput>
      <ExpectedOutput>
        <h1>Hello world from Blazor</h1>
      </ExpectedOutput>
    </SnapshotTest>
    
    지금은 컴백 스타일 테스트에 효과가 있다.
    나는 의외의 변경 사항을 얻을 수 있도록 많은 구성 요소에서 이 기능을 사용했다.이를 위해 나는 스냅샷 테스트가 매우 효과적이라고 생각한다.
    이것은 나의 OptionGrid 구성 요소를 테스트하는 데 매우 유용하다.이것은 아마도 나의 가장 복잡한 부분일 것이다. 대량의 반사와 동적 생성과 관련이 있다.결과적으로 조사 기간에 격자를 제공했다.

    기술적으로 말하자면, 나의 스냅숏 테스트는 하나의 통합 테스트와 같다. 왜냐하면 여러 개의 구성 요소를 사용하기 때문이다. 그러나 회귀 목적에서, 그것은 매우 훌륭하기 때문이다.테스트에서 사용할 샘플 모델을 정의했습니다.
    @inherits TestComponentBase
    
    <SnapshotTest Description="Options Grid renders correctly">
        <TestInput>
            <CascadingValue Value="EditContext">
                <OptionGrid NotApplicableLabel="N/A" TValue="Model" />
            </CascadingValue>
        </TestInput>
        <ExpectedOutput>
            <div class="option-grid">
                <span class="header"></span>
                <span class="header">N/A</span>
                <span class="header"></span>
                <span class="header">(Not) 1</span>
                <span class="header">2</span>
                <span class="header">3</span>
                <span class="header">4</span>
                <span class="header">(Very) 5</span>
                <hr>
                <span>Value 1 Name</span>
                <input type="radio" name="value-1" checked="">
                <span></span>
                <input type="radio" name="value-1">
                <input type="radio" name="value-1">
                <input type="radio" name="value-1">
                <input type="radio" name="value-1">
                <input type="radio" name="value-1">
                <i class="option-grid-row-description">Value 1 Description</i>
                <hr>
                <span>Value 2 Name</span>
                <input type="radio" name="value-2" checked="">
                <span></span>
                <input type="radio" name="value-2">
                <input type="radio" name="value-2">
                <input type="radio" name="value-2">
                <input type="radio" name="value-2">
                <input type="radio" name="value-2">
                <i class="option-grid-row-description">Value 2 Description</i>
                <hr>
                <span>Value 3 Name</span>
                <input type="radio" name="value-3" checked="">
                <span></span>
                <input type="radio" name="value-3">
                <input type="radio" name="value-3">
                <input type="radio" name="value-3">
                <input type="radio" name="value-3">
                <input type="radio" name="value-3">
                <i class="option-grid-row-description">Value 3 Description</i>
                <hr>
            </div>
        </ExpectedOutput>
    </SnapshotTest>
    
    @code
    {
        public EditContext EditContext = new EditContext(new Model());
    
        public class Model
        {
            [System.ComponentModel.DisplayName("Value 1 Name")]
            [System.ComponentModel.Description("Value 1 Description")]
            [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Value 1 is required")]
            public int Value1 { get; set; }
    
            [System.ComponentModel.DisplayName("Value 2 Name")]
            [System.ComponentModel.Description("Value 2 Description")]
            [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Value 2 is required")]
            public int Value2 { get; set; }
    
            [System.ComponentModel.DisplayName("Value 3 Name")]
            [System.ComponentModel.Description("Value 3 Description")]
            [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Value 3 is required")]
            public int Value3 { get; set; }
        }
    }
    
    예제 모델에서는 EditContext가 사용됩니다.<CascadingValue/>을 통해 구성 요소에 제공됩니다.
    나는 확실히 이 구성 요소를 둘러싼 다른 하위 구성 요소가 있지만, 이 스냅샷 테스트가 있으면 회귀에 대해 자신감을 가질 수 있다.

    NavigationManager를 사용한 유닛 테스트


    마이크로소프트는 이제 구체적인 대상이 아니라 항상 인터페이스를 사용하는 것을 배웠다. 많은 Blazor에서, 이것은 정말이다.
    불행히도 NavigationManager는 없습니다.
    Navigation Manager는 Blazor의 일부로서 응용 프로그램의 페이지를 변경하는 데 사용됩니다.
    따라서 Navigation Manager를 사용하는 구성 요소에 대한 단원 테스트가 관련되었을 때 문제가 발생했습니다.
    다행히도 Microsoft의 구체적인 구현 기록을 처리해야 하기 때문에 이것은 상당히 간단한 해결 방법이다.
    나는 간단히 INavigation Manager에 인터페이스를 만들었고, 그 다음은 간단한 패키지였다. 이 인터페이스를 지원하고 엔진 뚜껑 아래에서 구체적인 Navigation Manager를 호출한 다음, 내 구성 요소를 INavigation Manager를 사용하도록 설정했다.
    namespace SoftwareSurvey.Wrappers
    {
        public interface INavigationManager
        {
            string Uri { get; }
           void NavigateTo(string uri);
        }
    }
    
    using Microsoft.AspNetCore.Components;
    
    namespace SoftwareSurvey.Wrappers
    {
        public class NavigationManagerWrapper : INavigationManager
        {
            private readonly NavigationManager NavigationManager;
    
            public NavigationManagerWrapper(NavigationManager navigationManager)
            {
                NavigationManager = navigationManager;
            }
    
            public void NavigateTo(string uri) => NavigationManager.NavigateTo(uri);
    
            public string Uri => NavigationManager.Uri;
        }
    }
    
    <div class="form-group navigation-buttons">
        @if (HasPrevious)
        {
            <button type="button" class="btn previous-navigation-button" @onclick="HandlePrevious">Previous</button>
        }
        @if (HasNext)
        {
            <button type="submit" class="btn next-navigation-button" @onclick="HandleNext">Next</button>
        }
    
        <div class="clear"></div>
    </div>
    
    @code {
        [Inject]
        private SoftwareSurvey.Wrappers.INavigationManager NavigationManager { get; set; }
    
        ...
    }
    
    그리고 테스트와 관련이 있을 때 나는 Moq 라이브러리로 Mock<INavigationManager>을 만들고 쉽게 테스트를 진행할 수 있다.

    StateHasChanged()를 사용한 셀 테스트


    part 4에서 나는 나의 <PreRenderLoadingMessage /> 구성 요소에 대해 이야기했다.이것은 프로그램이 시작될 때 불러오는 메시지 (신호기 구축 등) 를 표시하고, 모든 것이 준비되었을 때 프로그램 내용을 표시합니다.
    단원 테스트는 StateHasChanged()을 사용했기 때문에 문제가 되었다.
    이상적인 경우, 나는 두 가지 테스트가 필요하다. 하나는 검증 프로그램이 이전에 표시된 불러오는 메시지를 준비하는 것이고, 다른 하나는 검증 프로그램이 준비한 후에 표시될 내용을 검증하는 것이다.
    StateHasChange () 의 문제는 Before 상태를 검증할 기회가 생기기 전에 두 번째 렌더링을 강제로 합니다.
    그래서 테스트를 위해, 나는 일반적으로 하지 말라고 권장하는 일을 했다. 관련 구성 요소에 단원 테스트에 특정한 코드를 추가하는 것이다.
    구성 요소에 UnitTestMode의 트리거 여부를 제어하기 위해 추가 인자 (StateHasChanged()) 를 추가했습니다.
    @if (HasRendered)
    {
        @ChildContent
    }
    else
    {
        <LoadingSpinner Title="Loading ..." />
    }
    
    
    @code {
        [Parameter]
        public bool UnitTestMode { get; set; }
    
        [Parameter]
        public RenderFragment ChildContent { get; set; }
    
        private bool HasRendered { get; set; }
    
        protected override void OnAfterRender(bool firstRender)
        {
            if (firstRender)
            {
                HasRendered = true;
    
                if (!UnitTestMode)
                {
                    StateHasChanged();
                }
            }
        }
    }
    
    그리고 내 테스트에서 나는 렌더링에 대해 더 많은 통제를 얻었다.UnitTestMode을 true로 설정하여 두 번째 렌더링의 정확한 시간을 지정할 수 있습니다.
            [Fact]
            public void ShowsLoadingMessage_OnFirstRender()
            {
                using var context = new TestContext();
                context.Services.AddSingleton(new Mock<IEventLoggingService>().Object);
    
                IRenderedComponent<PreRenderLoadingMessage> cut = context.RenderComponent<PreRenderLoadingMessage>(parameters =>
                    parameters
                        .Add(p => p.UnitTestMode, true)
                        .AddChildContent("<div>Hello World</div>")
                );
    
                Assert.Contains("Loading ...", cut.Markup);
            }
    
            [Fact]
            public void ShowsChildContent_OnSecondRender()
            {
                using var context = new TestContext();
                context.Services.AddSingleton(new Mock<IEventLoggingService>().Object);
    
                IRenderedComponent<PreRenderLoadingMessage> cut = context.RenderComponent<PreRenderLoadingMessage>(parameters => 
                    parameters
                        .Add(p => p.UnitTestMode, true)
                        .AddChildContent("<div>Hello World</div>")
                );
    
                Assert.DoesNotContain("<div>Hello World</div>", cut.Markup);
    
                cut.Render();
                Assert.Contains("<div>Hello World</div>", cut.Markup);
            }
    
    그리고, 단계별 confort를 제공하기 위해서, UnitTestMode가 언제 닫히는지 테스트했습니다. 우리는 StateHasChanged()으로 인해 자동으로 다시 렌더링될 것을 알고 있습니다.
            [Fact]
            public void ShowsChildContent_IfInDefaultMode()
            {
                using var context = new TestContext();
                context.Services.AddSingleton(new Mock<IEventLoggingService>().Object);
    
                IRenderedComponent<PreRenderLoadingMessage> cut = context.RenderComponent<PreRenderLoadingMessage>(parameters =>
                    parameters.AddChildContent("<div>Hello World</div>")
                );
    
                Assert.Contains("<div>Hello World</div>", cut.Markup);
            }
    
    내가 말한 바와 같이, 나는 보통 구성 요소에 이런 변경 사항을 직접 추가하지 말라고 조언한다. 특히 테스트를 위해서이다. 그러나 이런 상황에서, 나는 그것이 효과적이고 안전하다고 믿는다.
    나는 StateHasChanged()의 비동기 버전을 계획적으로 허용할 수 있다고 믿는다. 일부는 이것을 위한 것이고, 일부는 여러 개의 변경 사항을 대량으로 보여주기 위한 것이다. (성능을 위한 것) 그래서 일단 사용할 수 있다면, 나는 바뀔 수 있다.

    기타 예


    나는 아직 bUnit를 어떻게 사용하는지 상세하게 소개하지 않았다. 왜냐하면 나는 documentation site이 매우 잘했다고 믿기 때문이다.
    그러나 내가 어떻게 사용하는지에 대한 예를 더 보고 싶다면 github for the Software Survey UnitTests project을 보세요.

    다음


    다음에는 Selenium 및 xUnit을 사용한 엔드 투 엔드 테스트를 소개하겠습니다.

    좋은 웹페이지 즐겨찾기