Most of the applications I’m building should support multiple languages and cultures. This means that I also need to test my application code taking into account this into account.
In XUnit, there is no out-of-the-box way to change the culture of your unit test. However XUnit is easy to extend and you can even find a UseCultureAttribute example that exacty provides the functionality I need.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Globalization; | |
using System.Linq; | |
using System.Reflection; | |
using System.Threading; | |
using Xunit.Sdk; | |
/// <summary> | |
/// Apply this attribute to your test method to replace the | |
/// <see cref="Thread.CurrentThread" /> <see cref="CultureInfo.CurrentCulture" /> and | |
/// <see cref="CultureInfo.CurrentUICulture" /> with another culture. | |
/// </summary> | |
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |
public class UseCultureAttribute : BeforeAfterTestAttribute | |
{ | |
readonly Lazy<CultureInfo> culture; | |
readonly Lazy<CultureInfo> uiCulture; | |
CultureInfo originalCulture; | |
CultureInfo originalUICulture; | |
/// <summary> | |
/// Replaces the culture and UI culture of the current thread with | |
/// <paramref name="culture" /> | |
/// </summary> | |
/// <param name="culture">The name of the culture.</param> | |
/// <remarks> | |
/// <para> | |
/// This constructor overload uses <paramref name="culture" /> for both | |
/// <see cref="Culture" /> and <see cref="UICulture" />. | |
/// </para> | |
/// </remarks> | |
public UseCultureAttribute(string culture) | |
: this(culture, culture) { } | |
/// <summary> | |
/// Replaces the culture and UI culture of the current thread with | |
/// <paramref name="culture" /> and <paramref name="uiCulture" /> | |
/// </summary> | |
/// <param name="culture">The name of the culture.</param> | |
/// <param name="uiCulture">The name of the UI culture.</param> | |
public UseCultureAttribute(string culture, string uiCulture) | |
{ | |
this.culture = new Lazy<CultureInfo>(() => new CultureInfo(culture, false)); | |
this.uiCulture = new Lazy<CultureInfo>(() => new CultureInfo(uiCulture, false)); | |
} | |
/// <summary> | |
/// Gets the culture. | |
/// </summary> | |
public CultureInfo Culture { get { return culture.Value; } } | |
/// <summary> | |
/// Gets the UI culture. | |
/// </summary> | |
public CultureInfo UICulture { get { return uiCulture.Value; } } | |
/// <summary> | |
/// Stores the current <see cref="Thread.CurrentPrincipal" /> | |
/// <see cref="CultureInfo.CurrentCulture" /> and <see cref="CultureInfo.CurrentUICulture" /> | |
/// and replaces them with the new cultures defined in the constructor. | |
/// </summary> | |
/// <param name="methodUnderTest">The method under test</param> | |
public override void Before(MethodInfo methodUnderTest) | |
{ | |
originalCulture = Thread.CurrentThread.CurrentCulture; | |
originalUICulture = Thread.CurrentThread.CurrentUICulture; | |
Thread.CurrentThread.CurrentCulture = Culture; | |
Thread.CurrentThread.CurrentUICulture = UICulture; | |
CultureInfo.CurrentCulture.ClearCachedData(); | |
CultureInfo.CurrentUICulture.ClearCachedData(); | |
} | |
/// <summary> | |
/// Restores the original <see cref="CultureInfo.CurrentCulture" /> and | |
/// <see cref="CultureInfo.CurrentUICulture" /> to <see cref="Thread.CurrentPrincipal" /> | |
/// </summary> | |
/// <param name="methodUnderTest">The method under test</param> | |
public override void After(MethodInfo methodUnderTest) | |
{ | |
Thread.CurrentThread.CurrentCulture = originalCulture; | |
Thread.CurrentThread.CurrentUICulture = originalUICulture; | |
CultureInfo.CurrentCulture.ClearCachedData(); | |
CultureInfo.CurrentUICulture.ClearCachedData(); | |
} | |
} |
Here is an example where I used this attribute to test if my validation messages are translated correctly:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[UseCulture("nl-BE")] | |
[Fact] | |
public void Validator_TranslatesValidationMessages_Based_On_CurrentUICulture() | |
{ | |
//Arrange | |
var invalidOrder = new Order(); | |
invalidOrder.Orderlines.Add(new Orderline()); | |
//Act | |
var validator = _fixture.Container.Resolve<IValidator>(); | |
var result = validator.Validate(invalidOrder); | |
//Assert | |
Assert.False(result.IsValid); | |
Assert.Equal(5, result.ValidationErrors.Count); | |
Assert.Equal("'Id' mag niet leeg zijn.", result.ValidationErrors[0].ToString()); | |
Assert.Equal("'Name' mag niet leeg zijn.", result.ValidationErrors[1].ToString()); | |
Assert.Equal("'Email Address' mag niet leeg zijn.", result.ValidationErrors[2].ToString()); | |
Assert.Equal("'Product' mag niet leeg zijn.", result.ValidationErrors[3].ToString()); | |
Assert.Equal("'Amount' moet groter zijn dan of gelijk zijn aan '1'.", result.ValidationErrors[4].ToString()); | |
} |