Реализация фиктивного интерфейса с использованием Moq в C#

Я пытаюсь написать тесты пользовательского интерфейса Xamarin.Forms, используя Moq, чтобы имитировать мой интерфейс аутентификации: [предыдущий вопрос] [1]. Я реорганизовал свое приложение, так что мой метод SignIn(string username, string password) находится внутри класса, реализующего IAuthService. Теперь у меня возникают проблемы с имитацией IAuthService, чтобы по существу «заменить» фактическую проверку входа, которая происходит при нажатии кнопки Sign In. В моем классе CloudAuthService (который реализует IAuthService) я выполняю аутентификацию в Amazon Cognito, но хочу имитировать этот результат в тесте пользовательского интерфейса, чтобы он не вызывал облачную службу.

EDIT: после многих предложений я решил включить мою текущую реализацию ниже. Это все еще не работает полностью, несмотря на то, что вывод Console.WriteLine(App.AuthApi.IsMockService()); в методе BeforeEachTest() приводит к true (как и ожидалось). Однако выполнение того же самого в методе конструктора App() приводит к false. Таким образом, кажется, что он не работает до того, как приложение действительно запустится, есть ли способ получить код UITest, который запускается до инициализации приложения?

Страница входа

[XamlCompilation(XamlCompilationOptions.Compile)]
public sealed partial class LoginPage
{
    private readonly IBiometricAuthentication _bioInterface;

    private static readonly Lazy<LoginPage>
        Lazy =
            new Lazy<LoginPage>
                (() => new LoginPage(App.AuthApi));

    public static LoginPage Instance => Lazy.Value;

    private string _username;

    private string _password;

    private LoginPageViewModel _viewModel;

    private IAuthService _authService;

    public LoginPage(IAuthService authService)
    {
        InitializeComponent();
        _authService = authService;

        _viewModel = new LoginPageViewModel();

        BindingContext = _viewModel;
    }

    private void LoginButtonClicked(object sender, EventArgs args)
    {
          _username = UsernameEntry.Text;
          _password = PasswordEntry.Text;
          LoginToApplication();
    }

    public async void LoginToApplication()
    {
          AuthenticationContext context = await _authService.SignIn(_username, _password);
    }
}

Класс приложения

public partial class App
{
    public static IAuthService AuthApi { get; set; } = new AWSCognito()
    public App()
    {
        Console.WriteLine(AuthApi.IsMockService())
        // AuthApi = new AWSCognito(); // AWSCognito implements IAuthService
        InitializeComponent();
        MainPage = new NavigationPage(new LoginPage(AuthApi));
    }
}

Тестовый класс

class LoginPageTest
{
    IApp app;
    readonly Platform platform;

    public LoginPageTest(Platform platform)
    {
        this.platform = platform;
    }

    [SetUp]
    public void BeforeEachTest()
    {

        var mocker = new Mock<IAuthService>();

        var response = new AuthenticationContext(CognitoResult.Ok)
        {
            IdToken = "SUCCESS_TOKEN"
        };
        mocker.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(() => new MockAuthService().SignIn("a", "a"));
        mocker.Setup(x => x.IsMockService()).Returns(() => new MockAuthService().IsMockService());
        App.AuthApi = mocker.Object;
        Console.WriteLine(App.AuthApi.IsMockService());

        app = AppInitializer.StartApp(platform);
    }


    [Test]
    public void ClickingLoginWithUsernameAndPasswordStartsLoading()
    {
        app.WaitForElement(c => c.Marked("Welcome"));
        app.EnterText(c => c.Marked("Username"), new string('a', 1));
        app.EnterText(c => c.Marked("Password"), new string('a', 1));

        app.Tap("Login");

        bool state = app.Query(c => c.Class("ProgressBar")).FirstOrDefault().Enabled;

        Assert.IsTrue(state);
    }
}

person itcoder    schedule 14.01.2021    source источник
comment
В этом случае я просто проверяю, что приложение отображает панель загрузки после нажатия кнопки «Войти». Я пытаюсь смоделировать IAuthService, чтобы приложение не вызывало службу Amazon Cognito. Я обновлю вопрос с полной тестовой реализацией.   -  person itcoder    schedule 14.01.2021
comment
Вы не используете мокап. Это делает его неактуальным. Чтобы сделать ваш код пригодным для тестирования, app должен получить свой _authService член с помощью механизма, который позволяет его заменять.   -  person Aluan Haddad    schedule 14.01.2021
comment
@AluanHaddad это имеет смысл. Итак, если я сконструирую LoginPage так, чтобы он передавался экземпляру IAuthService в той или иной форме, то теоретически я должен иметь возможность заменить эту службу с помощью издевательской службы?   -  person itcoder    schedule 14.01.2021
comment
@itcoder да. В яблочко. Лучше всего использовать параметры конструктора для зависимостей, но изменяемое свойство может быть последним средством. См. раздел Xamarin Forms: внедрение зависимостей для примера использования DI в таком приложении.   -  person Aluan Haddad    schedule 14.01.2021
comment
@itcoder, что внедрение должно произойти раньше, чем когда вы попытаетесь установить это глобальное свойство. Экземпляр к тому времени уже был бы создан.   -  person Nkosi    schedule 14.01.2021
comment
Я не знаю, что идиоматично в Xamarin Forms, но использование одноэлементного шаблона кажется довольно неудобным.   -  person Aluan Haddad    schedule 15.01.2021
comment
@Nkosi Я немного не понимаю, как я мог ввести это свойство раньше. Разве метод public App() не запускается в первую очередь? Или вы имеете в виду ввести его раньше с точки зрения теста?   -  person itcoder    schedule 15.01.2021
comment
Обратите внимание, что для того, чтобы считаться актуальным для SO, вопрос должен включать встроенный код (как указал @Nkosi). Это редактирование, которое только автор кода может выполнять из-за лицензионных ограничений — для публикации кода на SO требуется как минимум CC. BY-SA 4, поэтому в большинстве случаев это не может сделать неавтор. Рассмотрите возможность отредактировать вопрос, чтобы избежать дальнейшего отрицательного/закрытого голосования.   -  person Alexei Levenkov    schedule 15.01.2021


Ответы (1)


Похоже, ваша проблема заключается в том, что вы внедрили макет после прохождения теста. Это означает, что при выполнении он использует оригинальный AuthService. Если мы изменим код, чтобы переместить инъекцию до того, как что-либо будет выполнено, мы должны увидеть ожидаемый результат:

    // let's bring this mock injection up here

    var mocker = new Mock<IAuthService>();

    mocker.Setup(x => x.SignIn(It.IsAny<string>(), It.IsAny<string>())).Returns(Task.FromResult(response)).Verifiable();
    App.AuthApi = mocker.Object;

    // now we try to login, which should call the mock methods of the auth service
    app.WaitForElement(c => c.Marked("Welcome to Manuly!"));
    app.EnterText(c => c.Marked("Username"), new string('a', 1));
    app.EnterText(c => c.Marked("Password"), new string('a', 1));

    app.Tap("Login");
    var response = new AuthenticationContext(CognitoResult.Ok)
    {
        IdToken = "SUCCESS_TOKEN",

    };


    bool state = app.Query(c => c.Class("ProgressBar")).FirstOrDefault().Enabled;

    Assert.IsTrue(state);

Теперь попробуйте выполнить его, и он должен работать так, как вы хотите.

РЕДАКТИРОВАТЬ: Как указано в комментариях Нкоси, статическая служба аутентификации устанавливается в конструкторе, предотвращая это.

Так что это тоже нужно будет изменить:

public partial class App
{
    public static IAuthService AuthApi { get; set; } =new AWSCognito(); // assign it here statically
    public App()
    {
            // AuthApi = new AWSCognito(); <-- remove this
        InitializeComponent();
        MainPage = new NavigationPage(new LoginPage(AuthApi));
    }
}
person ekke    schedule 14.01.2021
comment
Интересно, да, это логично. Даже в этом случае он все еще не заменяет оригинал, что довольно странно. Возможно, это связано с тем, что я использую шаблон Singleton на LoginPage, но я не уверен. - person itcoder; 15.01.2021
comment
Ага, только что видел - person ekke; 15.01.2021
comment
Использование статической инициализации в лучшем случае неоптимально. Это делает тесты хрупкими, а внедрение через изменяемое статическое свойство только для одной службы не может быть обобщено. - person Aluan Haddad; 15.01.2021
comment
Согласитесь, это очень некрасиво. Я настоятельно рекомендую статический одноэлементный контейнер IoC (например, TinyIoC), который можно настроить с фиктивными зависимостями. - person ekke; 15.01.2021
comment
Спасибо за ваши предложения. По какой-то причине приведенная выше реализация все еще не работает, и исходный AuthService все еще вызывается. Я посмотрю на TinyIoC. - person itcoder; 15.01.2021
comment
Я думал, что установка макета перед app = AppInitializer.StartApp(platform) сработает, но это тоже не так. - person itcoder; 15.01.2021
comment
Обновление: обновлен исходный вопрос с текущей (не работающей) реализацией. - person itcoder; 15.01.2021