【问题标题】:Test that thread culture was set in async controller action测试在异步控制器操作中设置了线程文化
【发布时间】:2020-03-16 07:13:05
【问题描述】:

我正在尝试迁移一个 asp.net mvc 5 控制器以使用 async/await。我在测试是否设置了正确的文化时遇到了问题。

在旧(非异步)版本的代码中,单元测试通过,因为一切都在同一个线程上运行。在新(异步)版本的代码中,单元测试失败,因为在调用 async/await 方法后未维护当前区域性。

在新版本的代码中,文化设置正确,视图有正确的文化设置。唯一的问题是我无法在我的 NUnit 测试中对此进行测试,因为它在 await 调用之外。

你知道我该如何解决这个问题吗?我在下面使用的代码提供了一个类似伪代码的版本。

旧代码:

public ActionResult Index()
{
    // Look up user in database
    var dbUser = database.GetUser(User.Identity.GetUserId());

    // Set preferred culture
    Thread.CurrentThread.CurrentCulture = new CultureInfo(dbUser.PreferredCulture);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(dbUser.PreferredCulture);

    // Do some other work
    DoSynchronousWork();

    return View();
}

[TestFixture]
public class TestClass
{
    [Test]
    public void TestIndex_CorrectCulture()
    {
        // Mock database
        database.GetUser().Returns(new User(){PreferredCulture = "de"});

        // Call controller
        _controller.Index();

        // Check that the thread culture was correctly set - this passes
        Assert.AreEqual("de", Thread.CurrentThread.CurrentCulture);
        Assert.AreEqual("de", Thread.CurrentThread.CurrentUICulture);
    }
}

新代码:

public async Task<ActionResult> Index()
{
    // Look up user in database
    var dbUser = database.GetUser(User.Identity.GetUserId());

    // Set preferred culture
    Thread.CurrentThread.CurrentCulture = new CultureInfo(dbUser.PreferredCulture);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(dbUser.PreferredCulture);

    // Do some other work
    await DoAsynchronousWork();

    return View();
}

[TestFixture]
public class TestClass
{
    [Test]
    public async Task TestIndex_CorrectCulture()
    {
        // Mock database
        database.GetUser().Returns(new User(){PreferredCulture = "de"});

        // Call controller
        await _controller.Index();

        // Check that the thread culture was correctly set - this fails because the culture inside _controller.Index() is lost after the await call
        Assert.AreEqual("de", Thread.CurrentThread.CurrentCulture);
        Assert.AreEqual("de", Thread.CurrentThread.CurrentUICulture);
    }
}

【问题讨论】:

  • 在我看来,测试这一点的最简单方法是在视图中包含与文化相关的内容。这样,您的测试就是测试是否返回了正确的视图(行为),而不是设置了线程的文化(实现)。

标签: c# asp.net-mvc nunit cultureinfo


【解决方案1】:

如果您使用的是模拟库,请尝试将不可测试的代码移至服务:

interface IThreadCultureSetter
{
    void SetCurrentThreadCulture(CultureInfo ci, [CallerMemberName]string callerMethod = null);
}

class ThreadCultureSetter : IThreadCultureSetter
{
    public void SetCurrentThreadCulture(CultureInfo ci, [CallerMemberName]string callerMethod = null)
    {
        // Set thread culture
    }
}

更改您的控制器操作以使用该界面:

private readonly IThreadCultureSetter _cultureSetter;

public async Task<ActionResult> Index()
{
    // Look up user in database
    var dbUser = database.GetUser(User.Identity.GetUserId());

    // Set preferred culture
    _cultureSetter.SetCurrentThreadCulture(new CultureInfo(dbUser.PreferredCulture));

    // Do some other work
    await DoAsynchronousWork();

    return View();
}

在您的测试中,提供一个模拟的IThreadCultureSetter 并验证该方法是否使用所需的CultureInfo 调用:

// Assume you use Moq
Mock<IThreadCultureSetter> cultureSetterMock;

// The SetCurrentThreadCulture must be called by Index method
mock.Verify(x => x.SetCurrentThreadCulture(It.Is<CultureInfo>(ci => ci.Name == "de"), "Index"));

事实上,Thread.CurrentThread.CurrentCulture 是异步静态的(值存储在 AsyncLocal 中)并且对测试不友好,因为它的值仅存在于当前捕获的异步范围内。

更新

我刚刚发现可以使用自定义 SynchronizationContext 来测试线程文化设置(无法保证)是否符合预期的文化:

class ThreadCultureInspectionSynchronizationContext : SynchronizationContext
{
    private readonly string _expectedCultureName;

    public ThreadCultureInspectionSynchronizationContext(string expectedCultureName)
    {
        _expectedCultureName = expectedCultureName;
    }

    public bool WasSetToExpected { get; private set; }

    public override void Post(SendOrPostCallback d, object state)
    {
        // When context is switching, e.g. continuation of an await being executed, this method is called.
        // This method could inspect on thread culture, and set flag to true once expected culture detected.
        // This method can only prove that the thread culture was set, but cannot suggest by which method.

        if (!WasSetToExpected)
        {
            WasSetToExpected = Thread.CurrentThread.CurrentCulture.Name.Dump() == _expectedCultureName;
        }

        base.Post(d, state);
    }
}

在你的测试中,在调用Index之前:

var ctx = new ThreadCultureInspectionSynchronizationContext("de");
SynchronizationContext.SetSynchronizationContext(ctx);
await Index();
...
Assert.IsTrue(ctx.WasSetToExpected);

如果您的线程文化是由Index 方法设置的,您会看到同步上下文对线程文化的所有检查都是您所期望的。

【讨论】:

  • 当然,这将是显而易见的答案,但是此方法不会验证是否已在返回视图的线程上设置了文化。如果将来有人将对_cultureSetter.SetCurrentThreadCulture(...) 的调用转移到异步方法中,那么此测试将通过。但是文化只会在那个异步方法中设置,而不是在实际返回视图的线程上。
  • 这很好。但是,应该对ICultureSetter 及其实现进行单元测试,以验证一旦方法更改为异步,它就会为父异步范围设置区域性。 ICultureSetter 的单元测试和控制器操作的单元测试总共将保证“操作设置文化并且文化设置正确”。
  • 该测试将验证ICultureSetter 是否正常工作,但它不会验证控制器操作是否正确调用它。为此,我们应该找到一种方法来测试该操作是否直接调用 ICultureSetter,而不是从另一个异步范围内。
  • 好吧。技术上可以通过将[CallerMemberName] string callerMethod = null 参数添加到SetCurrentThreadCulture 来检查ICultureSetter 是否必须由操作方法调用,这将告诉编译器注入调用方方法的名称。不过,我觉得我们可能已经进入了一个兔子洞 :) 可能已经过度测试了该方法。
  • 是的,我认为你是对的。这个解决方案可能并不完美,但我认为这是可以做到的最好的。我将把这个问题留待一段时间,如果没有更好的结果,我将把它标记为答案。谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-12-04
  • 2019-01-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多