Test that thread culture was set in async controller action

Viewed 370

I'm trying to migrate an asp.net mvc 5 controller to use async/await. I have a problem with testing that the correct culture has been set.

In the old (non-async) version of the code, the unit test passes because everything is running on the same thread. In the new (async) version of the code, the unit test fails because the current culture is not maintained after calling an async/await method.

In the new version of the code, the culture is set correctly, and the view has the correct culture set. The only problem is that I can't test this in my NUnit test, because it's outside of an await call.

Do you know how I can fix this? I have provided a pseudocode-like version of the code I use below.

Old code:

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);
    }
}

New code:

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);
    }
}
1 Answers

If you are using mock library, try moving code not testable to a service:

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
    }
}

Change your controller action to use the interface:

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();
}

In your test, provide a mocked IThreadCultureSetter and verify that the method was called with desired 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"));

The fact is that Thread.CurrentThread.CurrentCulture is async static (value stored in AsyncLocal) and is not test friendly, because its value only exists for current captured async scope.

UPDATE

I just figured out it might be possible to use a custom SynchronizationContext to test the thread culture was set (can't guarantee by what) as expected culture:

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);
    }
}

In your test, before calling Index:

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

If your thread culture was set by Index method, you would expect to see all inspection on thread culture by the synchronization context is your expected one.

Related