AvaloniaUI 앱에서 UI 테스트를 자동화하는 방법

안녕하세요, 이번 포스트에서는 자동화된 UI 테스팅에 대해 자세히 설명하겠습니다. 항상 그렇듯이 내 앱Camelot을 작업 예제로 사용할 것입니다.

UI 테스트가 필요한 이유



UI 테스트를 자동화해야 하는 이유는 무엇입니까? 수동 테스트에 비해 시간이 절약되기 때문입니다. 더 정확하고 회귀 버그를 찾는 데 도움이 됩니다. 여기서 유일한 단점은 테스트 작성 및 초기 설정에 소요되는 시간입니다.

UI 테스트가 할 수 있는 일



UI 테스트는 사용자가 하는 모든 것을 에뮬레이트할 수 있습니다. 마우스 이동, 클릭, 키 누르기 등. 수동 테스트에서와 같이 테스트에서 단일 테스트 케이스/시나리오를 확인하는 것이 좋습니다. 내 앱의 예: 디렉토리 생성 대화 상자를 열고 디렉토리 이름을 입력하고 생성하고 생성되었는지 관찰합니다. 이 테스트는 쉽게 자동화될 수 있습니다. 스모크 테스트에서 자동화를 시작하고 전체 회귀 테스트로 마무리하는 것이 좋습니다.

AvaloniaUI에서 UI 테스트



AvaloniaUI에는 앱 설정 후 몇 초 후에 사용자 정의 코드를 실행할 수 있는 기능이 있습니다. 거기에 테스트 코드를 추가하고 시나리오를 실행할 수 있습니다. AvaloniaUI 0.10.0은 헤드리스 플랫폼도 도입했습니다. 헤드리스 앱은 UI가 없으므로 GUI가 아닌 OS에서도 시작할 수 있습니다. GUI가 없는 서버에서 테스트를 실행하는 데 유용할 수 있습니다. github 작업을 통해 Github에서 UI 테스트를 실행하므로 이 옵션이 유용합니다. 실제 앱을 실행하고 테스트를 실행하는 것을 선호하지만 전체 앱을 실행하지 않고 컨트롤 등을 테스트할 수도 있습니다.

내부의 Avalonia 앱은 정적 필드 등을 사용하므로 테스트당 앱의 새 인스턴스를 생성할 수 없습니다. 단일 인스턴스를 생성하고 테스트 전체에서 재사용해야 하므로 UI ​​테스트 프로젝트에서 테스트의 병렬 실행을 비활성화해야 합니다. 또한 모든 테스트에는 적절한 정리가 있어야 합니다. 그렇지 않으면 다른 테스트가 중단될 수 있습니다.

UI 테스트를 위한 인프라 설정



테스트를 위해 Xunit을 사용하는데 UI 테스트에서는 기본적으로 작동하지 않습니다. Xunit에 내 러너를 추가해야 했습니다.

private class Runner : XunitTestAssemblyRunner
{
    // constructor

    public override void Dispose()
    {
        AvaloniaApp.Stop(); // cleanups existing avalonia app instance

        base.Dispose();
    }

// this method is called only if test parallelization is enabled. I had to enable it and set max parallelization limit to 1 in order to avoid parallel tests execution
    protected override void SetupSyncContext(int maxParallelThreads)
    {
        var tcs = new TaskCompletionSource<SynchronizationContext>();
        var thread = new Thread(() =>
        {
            try
            {
                // DI registrations
                AvaloniaApp.RegisterDependencies();

                AvaloniaApp
                    .BuildAvaloniaApp()
                    .AfterSetup(_ =>
                    {
                        // sets sync context for tests. avalonia UI runs in it's own single thread, updates from other threads are not allowed
                        tcs.SetResult(SynchronizationContext.Current);
                    })
                    .StartWithClassicDesktopLifetime(new string[0]); // run app as usual
            }
            catch (Exception e)
            {
                tcs.SetException(e);
            }
        })
        {
            IsBackground = true
        };

        thread.Start();

        SynchronizationContext.SetSynchronizationContext(tcs.Task.Result);
    }
}


Full code
One more headless tests example

또한 실제 앱의 래퍼인 AvaloniaApp 클래스를 추가했습니다.

public static class AvaloniaApp
{
    // DI registrations
    public static void RegisterDependencies() =>
        Bootstrapper.Register(Locator.CurrentMutable, Locator.Current);

    // stop app and cleanup
    public static void Stop()
    {
        var app = GetApp();
        if (app is IDisposable disposable)
        {
            Dispatcher.UIThread.Post(disposable.Dispose);
        }

        Dispatcher.UIThread.Post(() => app.Shutdown());
    }

    public static MainWindow GetMainWindow() => (MainWindow) GetApp().MainWindow;

    public static IClassicDesktopStyleApplicationLifetime GetApp() =>
        (IClassicDesktopStyleApplicationLifetime) Application.Current.ApplicationLifetime;

    public static AppBuilder BuildAvaloniaApp() =>
        AppBuilder
            .Configure<App>()
            .UsePlatformDetect()
            .UseReactiveUI()
            .UseHeadless(); // note that I run app as headless one
}


UI 테스트



이제 테스트를 작성할 시간입니다! 다음은 F1를 통해 대화 상자를 여는 테스트의 예입니다.

public class OpenAboutDialogFlow : IDisposable
{
    private AboutDialog _dialog;

    [Fact(DisplayName = "Open about dialog")]
    public async Task TestAboutDialog()
    {
        var app = AvaloniaApp.GetApp();
        var window = AvaloniaApp.GetMainWindow();
        // wait for initial setup
        await Task.Delay(100);

        Keyboard.PressKey(window, Key.Tab); // hack for focusing file panel, in headless tests it's not focused by default
        Keyboard.PressKey(window, Key.Down);
        Keyboard.PressKey(window, Key.F1); // press F1

        await Task.Delay(100); // UI is not updated immediately so I had to add delays everywhere

        _dialog = app
            .Windows
            .OfType<AboutDialog>()
            .SingleOrDefault();
        Assert.NotNull(_dialog);

        await Task.Delay(100);

        var githubButton = _dialog.GetVisualDescendants().OfType<Button>().SingleOrDefault();
        Assert.NotNull(githubButton);
        Assert.True(githubButton.IsDefault);
        Assert.True(githubButton.Command.CanExecute(null));
    }

    public void Dispose() => _dialog?.Close(); // cleanup: close dialog
}


간단해 보이죠? 지연을 위해 몇 가지 추가 코드를 추가해야 했지만 실제로 테스트는 간단합니다. 다음은 지연 서비스의 예입니다.

public static class WaitService
{
    public static async Task WaitForConditionAsync(Func<bool> condition, int delayMs = 50, int maxAttempts = 20)
    {
        for (var i = 0; i < maxAttempts; i++)
        {
            await Task.Delay(delayMs);

            if (condition())
            {
                break; // stop waiting for condition
            }
        }
    }
}


More tests examples
Official example

결론



AvalononiaUI는 UI 테스트를 실행하기 위한 좋은 인프라를 제공합니다. 많은 시간을 절약할 수 있기 때문에 충분히 복잡한 경우 프로젝트에서 사용하는 것이 좋습니다. 프로젝트에서 UI 테스트를 사용하고 있습니까? 댓글로 알려주세요

좋은 웹페이지 즐겨찾기