在C#中设置异步单元测试

6

我需要为我的每个单元测试都设置一个已登录的用户,这就迫使我在测试SetUp中进行异步调用(即登录)。

我找不到一种方法来使这个工作,我要么得到空指针异常,要么得到设置无效的签名。

public async void SetUp() {}

这使得我的所有测试都失败了,可能是因为我没有登录。
public async Task SetUp() {}

由于设置具有无效的签名,导致我的所有测试都被忽略。
我不想在每个测试中复制 X 行设置,因为它们完全相同,并且这就是设置的作用。
我错过了什么?这似乎是一个微不足道的问题。
为了展示一些内容,以下是我现在拥有的内容。
CreateTicketViewModel _viewModel;

        [SetUp()]
        public async void SetUp()    //I have tried using Task instead of void
        {

            IUserService userService = Dependency.Instance.Resolve<IUserService>();
            await userService.LoginAsync(this.UserName, this.Password);

            _viewModel = Dependency.Instance.Resolve<CreateTicketViewModel>();
        }

        [TearDown()]
        public void TearDown()
        {
            _viewModel = null;  // I have tried removing this
        }

        [Test()]
        public void Initialization()
        {
            // If I put what's in SetUp here and add "async" before void,
            // it works just fine

            Assert.IsNotNull(_viewModel);
            Assert.IsNotNull(_viewModel.Ticket);
        }

我已经阅读了许多关于在SUT中不要使用async void的博客文章,而我没有使用,但是这些博客文章都没有谈到测试本身。我是否有设计问题? - Gil Sand
你不能将测试方法改为异步吗?或者你可以将 SetUp 方法改为同步。虽然前者更推荐。 - Sriram Sakthivel
一个只包含断言的异步测试会显示警告。如果可能的话,我希望避免这种情况。 - Gil Sand
我不明白你的代码是如何工作的。它只在Setup方法中使用了IUserService吗? - Sriram Sakthivel
我正在使用UserService来登录自己(在测试情况下使用硬编码数据),但这是隐藏的。 然后通过依赖注入,我创建了我的ViewModel,它需要在其构造函数中登录。 - Gil Sand
基本上,我所有的测试都需要一个已初始化的视图模型,这个视图模型需要自己登录,所以我在每个测试之前都会登录。 - Gil Sand
3个回答

5

根据您使用的单元测试框架,async设置可能无法正确处理框架。在NUnit的情况下,我认为它还不支持异步安装程序方法。

因此,在设置中,您应该只是同步等待登录完成:

userService.LoginAsync(this.UserName, this.Password).Wait();

编辑:看起来这是一个未解决的问题https://github.com/nunit/nunit/issues/60

对于MSTests也是如此。


2
你可以将SetUp方法改为非异步方法,并编写以下代码:
new Task(() => userService.LoginAsync(this.UserName, this.Password)).RunSynchronously()

当我这样做时,当测试开始时,我的userService的内容仍然为空,因此失败。 请注意,在LoginAsync方法后面还有更多的异步调用。 - Gil Sand
虽然我喜欢同步运行登录的想法。 - Gil Sand

2

不支持异步设置,但支持异步测试方法。您可以将测试方法改为异步而不是设置方法。

[TestFixture]
public class AsyncSetupTest
{
    private Task<CreateTicketViewModel> viewModelTask;

    [SetUp()]
    public void SetUp()
    {
        viewModelTask = Task.Run(async () =>
        {
            IUserService userService = Dependency.Instance.Resolve<IUserService>();
            await userService.LoginAsync(this.UserName, this.Password);

            return Dependency.Instance.Resolve<CreateTicketViewModel>();
        });
    }

    [Test()]
    public async Task Initialization()
    {
        CreateTicketViewModel viewModel = await viewModelTask;

        Assert.IsNotNull(viewModel);
        Assert.IsNotNull(viewModel.Ticket);
    }
}

想法是,不要在Setup方法中完成所有设置工作,而是创建一个代表设置完成的Task,并在Test方法中等待它的完成。
这样就不需要重复所有的设置逻辑,只需从所有测试方法中提取Task中的ViewModel即可。

这个方案可以工作,但仍需要在每个测试中重复一行代码;但确实比我之前做的好。谢谢。 另一方面,Dan Dinu的答案提供了一个解决方案,可以直接使用,并且只需要在我的测试中编写断言,这意味着在设置中有更少的代码和更小的代码块。 - Gil Sand
@Zil 这个方法可以行得通。但在异步代码中阻塞永远不是一个好主意。你可能会遇到死锁也可以参考这篇文章 - Sriram Sakthivel
我同意生产代码,但我希望我的测试在登录过程中一直等待。您认为在这种特定情况下这样做是否不好?请注意,除了登录本身内部的异步调用外,我这里没有进行任何其他异步调用。 - Gil Sand
对于测试而言,阻塞可能是可以接受的。但如果安装了一些自定义同步上下文,则即使是单元测试也可能死锁。(修改了评论以添加链接) - Sriram Sakthivel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接