3

In .NET (VB or C#) and using Microsoft.VisualStudio.TestTools.UnitTesting for Unit testing:

How do I effectively change the locale decimal separator locally within a unit test so as String.Format("{0:0.0}", aDouble) (which AFAIK is locale-dependent) would generate strings with the modified locale?

EDIT: Observe: I am not asking how to output text with an specific locale. I am asking how to change the decimal separator of locale within a unit test so as to simulate what will happen in a system that has a different decimal separator. I am not calling to String.Format from within the unit test code, String.Format is being called from within the tested functionality.

Additional information:

I am creating a .NET library in VB and I have one class MyClass with an Encode(...) function that is writing, among other things, numeric information as text.

The component will be used in an environment where different computers might have different configuration for the "decimal separator" (comma or point). My component, should however be insensitive to this, meaning that it should always output "point" (for example by making use of the System.Globalization.CultureInfo.InvariantCulture when formatting numbers).

I wanted to write a unit test to ensure that the encoding functionality will continue to work even when the system locale decimal separator is set to "comma" instead of "point". I did some research and came up with something like this:

 Public Sub Encode_CultureWithCommaSeparator_OutputMatchesTemplate()
  ...
  Dim oldCulture = Threading.Thread.CurrentThread.CurrentCulture
  ' A culture that uses "," as decimal separator
  Threading.Thread.CurrentThread.CurrentCulture = New Globalization.CultureInfo("es-ES")
  CompareEncodedToTemplate(...)
  Threading.Thread.CurrentThread.CurrentCulture = oldCulture
End Sub

The CompareEncodedToTemplate function will use the MyClass.Encode method to write the information to a MemoryStream that then will be compared to a Template text file line per line, and the test will fail when they are not equal.

What I wanted to "simulate" was how the Encode function would operate when the locale has decimal separator different than "point". Apparently my test function is not working as I expected:

I ran the test in a computer where the decimal separator i set to point, and the test succeed, so I thought "my encode function will work as I want, because the test passed".

However then I ran the test in a computer with the decimal separator set to comma, and the test failed. I realized that this was because in my Encode logic I had missed to use the InvariantCulture when formatting a double. That means that my test was not working as expected, because I should have been able to detect this situation the first computer (as it was the reason for which I wanted to create the test).

Thank you in advance.

dacucar
  • 206
  • 3
  • 10
  • One more comment: I have the unit tests and the tested functionality in two different assemblies, in case that could make any difference... – dacucar Nov 17 '15 at 15:59
  • I think the main issue with `Encode_CultureWithCommaSeparator_OutputMatchesTemplate` is that you construct a new instance of CultureInfo instead of accessing the correct culture info from the global set like this: `System.Globalization.CultureInfo.GetCultureInfo("es-ES");` – Glenn Ferrie Nov 17 '15 at 16:01
  • Ok, I found the problem and it has more to do with how I designed the test class. I was instantiating the MyClass before actually changing the culture and for simplifying my explanation I said that the "encode" function was the one writing with String.Format, but in reality the numbers are converted to text in MyClass (this is on purpose and). Unfortunately, the problem essence was not captured in my question, but I believe both Glenn Ferrie and Jonathan Dickinson are correct answers to the question as I wrote it. The one from J.D is more "elaborated" but G.F was the one that made me react. – dacucar Nov 18 '15 at 08:15
  • One more comment: creating a new instance CultureInfo("es-ES") will also work. The problem as I said has to do with your comment "The trick was to set the culture info for the thread within the test before you run the operation you are testing." – dacucar Nov 18 '15 at 08:21
  • I will mark Jonathan Dickinson's answers as the right one, as he was the first to actually answer to what I was asking. But thanks to everyone! – dacucar Nov 18 '15 at 08:27

4 Answers4

5

Ran into exactly this issue with my development machine vs. our CI server. I wrote the follow struct to help:

public struct CultureContext : IDisposable
{
    public static readonly CultureInfo CommaCulture = new CultureInfo("en-us")
    {
        NumberFormat =
        {
            CurrencyDecimalSeparator = ",",
            NumberDecimalSeparator = ",",
            PercentDecimalSeparator = ","
        }
    };

    public static readonly CultureInfo PointCulture = new CultureInfo("en-us")
    {
        NumberFormat =
        {
            CurrencyDecimalSeparator = ".",
            NumberDecimalSeparator = ".",
            PercentDecimalSeparator = "."
        }
    };

    private readonly CultureInfo _originalCulture;

    public CultureContext(CultureInfo culture)
    {
        _originalCulture = Thread.CurrentThread.CurrentCulture;
        Thread.CurrentThread.CurrentCulture = culture;
    }

    public void Dispose()
    {
        Thread.CurrentThread.CurrentCulture = _originalCulture;
    }

    public static void UnderBoth(Action test)
    {
        using (new CultureContext(PointCulture))
        {
            test();
        }

        using (new CultureContext(CommaCulture))
        {
            test();
        }
    }
}

You can then test it as such:

CultureContext.UnderBoth(() => Assert.AreEqual("1.1", sut.ToString()));
Jonathan Dickinson
  • 9,050
  • 1
  • 37
  • 60
3

You should consider using NumberFormatInfo like this:

var nfi = new System.Globalization.NumberFormatInfo();
nfi.NumberDecimalSeparator = ",";
var formatted = (10.01).ToString("N", nfi);

NOTE: NumberFormatInfo has separate settings for currency vs. other numbers

from MSDN: https://msdn.microsoft.com/en-us/library/system.globalization.numberformatinfo(v=vs.110).aspx

Glenn Ferrie
  • 10,290
  • 3
  • 42
  • 73
  • Thank you but I am not asking how to format a text using a locale. I can create an NumberFormatInfo, but I am not going to pass that to the Encode function (which is the one writing the text). What I am trying is to create a Test that can ensure that the Encode function will work independently on how the System has configured the locale. Therefore I need to simulate a "change in the locale decimal point" within the unit test. – dacucar Nov 17 '15 at 15:26
  • This is still the right answer. The real problem is that the described Encode() function was not designed for unit testing. – Joel Coehoorn Nov 17 '15 at 15:59
  • I am sorry, but I do not agree. The proposed solution is, on the first hand making the Encode function to output "," instead of decimal point, which is not the intended behavior. Or somehow suggesting to pass a "NumberFormatInfo" to the Encode function. Glenn Ferrie's other answer is actually addressing the real issue. Same as Jonathan Dickinson. – dacucar Nov 18 '15 at 08:20
1

Here is a class that has a method like you described:

public class Class1
{
    public string FormatSpecial(double d) {
        return string.Format("{0:0.0}", d);
    }
}

Here are my unit tests:

[TestClass]
public class UnitTest1
{
    Sample.Class1 instance;

    [TestInitialize]
    public void InitTests()
    {
        instance = new Sample.Class1();
    }

    [TestMethod]
    public void TestMethod1()
    {
        var result = instance.FormatSpecial(5.25);
        Assert.AreEqual("5.3", result);
    }

    [TestMethod]
    public void TestMethod2()
    {
        Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("es-ES");
        var result = instance.FormatSpecial(5.25);
        Assert.AreEqual("5,3", result);
    }
}

Both of these tests execute successfully.

proof

The trick was to set the culture info for the thread within the test before you run the operation you are testing.

Glenn Ferrie
  • 10,290
  • 3
  • 42
  • 73
  • Your "trick" was helpful because it made me think again about how I had written the test. Thank you. – dacucar Nov 18 '15 at 08:54
0

As far as I know, formatting a string with a "{0.0}" will always render as a decimal.

String.Format("{0:0}", 1.3); // Prints 1.3 regardless of culture

You have to specify the it more generically as such:

String.Format("{0:f}", 1.3); // Prints 1,3 if de-DE for example

See the standard number formatting of strings here. And then based on the current culture in the context of the unit testing scope, it will render the string accordingly.

For example:

  • 1234.567 ("F", en-US) -> 1234.57
  • 1234.567 ("F", de-DE) -> 1234,57
  • 1234 ("F1", en-US) -> 1234.0
  • 1234 ("F1", de-DE) -> 1234,0

There are countless examples of formatting numbers within the aforementioned link. I hope you find this helpful.

David Pine
  • 23,787
  • 10
  • 79
  • 107
  • I can ensure that String.Format("{0:0.0}", value) is being rendered with decimal comma in a system where Swedish is used as the locale. – dacucar Nov 17 '15 at 15:22
  • That is different than String.Format("{0.0}", value), no? – David Pine Nov 17 '15 at 15:23
  • Sorry, no matter how much care one puts to write a post... one always make a mistake. I will fix in the main post. Thank you. – dacucar Nov 17 '15 at 15:28