수색…


뷰 모델 테스트하기

우리가 시작하기 전에 ...

애플리케이션 계층 측면에서 볼 때 ViewModel은 모든 비즈니스 로직과 규칙을 포함하는 클래스로, 요구 사항에 따라 애플리케이션을 수행합니다. 또한 UI, 데이터 레이어, 기본 기능 및 API 호출 등에 대한 참조를 가능한 한 많이 독립적으로 만드는 것이 중요합니다.이 모든 것이 VM을 테스트 할 수있게합니다.
즉, ViewModel :

  • UI 클래스 (보기, 페이지, 스타일, 이벤트)에 의존해서는 안됩니다.
  • 가능한 한 다른 클래스의 정적 데이터를 사용하지 않아야합니다.
  • 비즈니스 로직을 구현하고 UI에 있어야 할 데이터를 준비해야합니다.
  • 종속성 삽입을 사용하여 해결되는 인터페이스를 통해 다른 구성 요소 (데이터베이스, HTTP, UI 관련)를 사용해야합니다.

ViewModel에는 다른 VM 유형의 속성도있을 수 있습니다. 예를 들어, ContactsPageViewModelObservableCollection<ContactListItemViewModel>

비즈니스 요구 사항

다음과 같은 기능을 구현한다고 가정 해 보겠습니다.

As an unauthorized user
I want to log into the app
So that I will access the authorized features

사용자 이야기를 명확히 한 후에 다음과 같은 시나리오를 정의했습니다.

Scenario: trying to log in with valid non-empty creds
  Given the user is on Login screen
   When the user enters 'user' as username
    And the user enters 'pass' as password
    And the user taps the Login button
   Then the app shows the loading indicator
    And the app makes an API call for authentication

Scenario: trying to log in empty username
  Given the user is on Login screen
   When the user enters '  ' as username
    And the user enters 'pass' as password
    And the user taps the Login button
   Then the app shows an error message saying 'Please, enter correct username and password'
    And the app doesn't make an API call for authentication

우리는이 두 시나리오에만 머물러있을 것입니다. 물론 더 많은 경우가 있어야하며 실제로 코딩하기 전에 모두 정의해야합니다.하지만 뷰 모델의 단위 테스트에 익숙해지기에 충분합니다.

고전적인 TDD 접근 방식을 따르고 테스트중인 빈 클래스를 작성하는 것으로 시작합시다. 그런 다음 테스트를 작성하고 비즈니스 기능을 구현하여 녹색으로 만듭니다.

공통 수업

public abstract class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

서비스

뷰 모델이 UI 및 HTTP 클래스를 직접 사용해서는 안된다는 것을 기억하십니까? 대신 구현 세부 사항에 의존하지 말고 추상화로 정의해야합니다.

/// <summary>
/// Provides authentication functionality.
/// </summary>
public interface IAuthenticationService
{
    /// <summary>
    /// Tries to authenticate the user with the given credentials.
    /// </summary>
    /// <param name="userName">UserName</param>
    /// <param name="password">User's password</param>
    /// <returns>true if the user has been successfully authenticated</returns>
    Task<bool> Login(string userName, string password);
}

/// <summary>
/// UI-specific service providing abilities to show alert messages.
/// </summary>
public interface IAlertService
{
    /// <summary>
    /// Show an alert message to the user.
    /// </summary>
    /// <param name="title">Alert message title</param>
    /// <param name="message">Alert message text</param>
    Task ShowAlert(string title, string message);
}

ViewModel 스텁 만들기

이제 로그인 화면 용 페이지 클래스를 만들 겠지만 먼저 ViewModel부터 시작해 보겠습니다.

public class LoginPageViewModel : BaseViewModel
{
    private readonly IAuthenticationService authenticationService;
    private readonly IAlertService alertService;

    private string userName;
    private string password;
    private bool isLoading;

    private ICommand loginCommand;

    public LoginPageViewModel(IAuthenticationService authenticationService, IAlertService alertService)
    {
        this.authenticationService = authenticationService;
        this.alertService = alertService;
    }

    public string UserName
    {
        get
        {
            return userName;
        }
        set
        {
            if (userName!= value)
            {
                userName= value;
                OnPropertyChanged();
            }
        }
    }
    
    public string Password
    {
        get
        {
            return password;
        }
        set
        {
            if (password != value)
            {
                password = value;
                OnPropertyChanged();
            }
        }
    }

    public bool IsLoading
    {
        get
        {
            return isLoading;
        }
        set
        {
            if (isLoading != value)
            {
                isLoading = value;
                OnPropertyChanged();
            }
        }
    }

    public ICommand LoginCommand => loginCommand ?? (loginCommand = new Command(Login));

    private void Login()
    {
        authenticationService.Login(UserName, Password);
    }
}

UI에 바인딩 할 두 개의 string 속성과 명령을 정의했습니다. 페이지 클래스, XAML 마크 업을 작성하는 방법을 설명하지는 않으며이 주제에서 ViewModel에 구체적인 내용이 없으므로 바인딩합니다.

LoginPageViewModel 인스턴스를 만드는 방법은 무엇입니까?

난 당신이 아마 그냥 생성자와 VM을 만드는 것 같아요. 이제 VM이 생성자 매개 변수로 주입되는 2 개의 서비스에 의존하므로 var viewModel = new LoginPageViewModel() 만 할 수는 없습니다. var viewModel = new LoginPageViewModel() . Dependency Injection에 익숙하지 않다면 그것에 대해 배우는 것이 가장 좋은 순간입니다. 이 원칙을 모르고 따르지 않으면 올바른 단위 테스트가 불가능합니다.

테스트

이제 위에 나열된 사용 사례에 따라 몇 가지 테스트를 작성해 보겠습니다. 먼저 새 어셈블리 (클래스 라이브러리 만 만들거나 Microsoft 단위 테스트 도구를 사용하려는 경우 특수 테스트 프로젝트 선택)를 만들어야합니다. ProjectName.Tests 와 같은 이름을 지정하고 원래 PCL 프로젝트에 대한 참조를 추가하십시오.

이 예제에서는 NUnitMoq 를 사용할 것이지만, 당신의 선택에 대한 테스트 라이브러리를 계속 사용할 수 있습니다. 특별한 것은 없습니다.

좋습니다, 그것은 테스트 클래스입니다.

[TestFixture]
public class LoginPageViewModelTest
{
}

테스트 작성

처음 두 시나리오의 테스트 방법은 다음과 같습니다. 예상 결과 1 개당 1 개의 테스트 방법을 유지하고 한 번의 테스트에서 모든 것을 검사하지 마십시오. 그러면 코드에서 실패한 부분에 대한 명확한 보고서를받을 수 있습니다.

[TestFixture]
public class LoginPageViewModelTest
{
    private readonly Mock<IAuthenticationService> authenticationServiceMock =
        new Mock<IAuthenticationService>();
    private readonly Mock<IAlertService> alertServiceMock =
        new Mock<IAlertService>();
    
    [TestCase("user", "pass")]
    public void LogInWithValidCreds_LoadingIndicatorShown(string userName, string password)
    {
        LoginPageViewModel model = CreateViewModelAndLogin(userName, password);

        Assert.IsTrue(model.IsLoading);
    }

    [TestCase("user", "pass")]
    public void LogInWithValidCreds_AuthenticationRequested(string userName, string password)
    {
        CreateViewModelAndLogin(userName, password);

        authenticationServiceMock.Verify(x => x.Login(userName, password), Times.Once);
    }

    [TestCase("", "pass")]
    [TestCase("   ", "pass")]
    [TestCase(null, "pass")]
    public void LogInWithEmptyuserName_AuthenticationNotRequested(string userName, string password)
    {
        CreateViewModelAndLogin(userName, password);

        authenticationServiceMock.Verify(x => x.Login(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
    }

    [TestCase("", "pass", "Please, enter correct username and password")]
    [TestCase("   ", "pass", "Please, enter correct username and password")]
    [TestCase(null, "pass", "Please, enter correct username and password")]
    public void LogInWithEmptyUserName_AlertMessageShown(string userName, string password, string message)
    {
        CreateViewModelAndLogin(userName, password);

        alertServiceMock.Verify(x => x.ShowAlert(It.IsAny<string>(), message));
    }

    private LoginPageViewModel CreateViewModelAndLogin(string userName, string password)
    {
        var model = new LoginPageViewModel(
            authenticationServiceMock.Object,
            alertServiceMock.Object);

        model.UserName = userName;
        model.Password = password;

        model.LoginCommand.Execute(null);

        return model;
    }
}

우리가 간다:

여기에 이미지 설명을 입력하십시오.

이제 목표는 ViewModel의 Login 메소드에 대한 올바른 구현을 작성하는 것입니다.

비즈니스 로직 구현

private async void Login()
{
    if (String.IsNullOrWhiteSpace(UserName) || String.IsNullOrWhiteSpace(Password))
    {
        await alertService.ShowAlert("Warning", "Please, enter correct username and password");
    }
    else
    {
        IsLoading = true;
        bool isAuthenticated = await authenticationService.Login(UserName, Password);
    }
}

그리고 테스트를 다시 실행 한 후에 :

여기에 이미지 설명을 입력하십시오.

이제 코드를보다 안정적이고 회귀 - 안전성을 보장하는 새로운 테스트로 계속 커버 할 수 있습니다.



Modified text is an extract of the original Stack Overflow Documentation
아래 라이선스 CC BY-SA 3.0
와 제휴하지 않음 Stack Overflow