machine / machine.specifications

Machine.Specifications is a Context/Specification framework for .NET that removes language noise and simplifies tests.
MIT License
885 stars 178 forks source link

Add SetCulture attribute #523

Closed DmitryMak closed 2 months ago

DmitryMak commented 2 months ago

There seem to be no standard way to set a Culture for a spec. Basically the specs end up inheriting Culture from a runner.

Could you add something like NUnit's SetCulture attribute

robertcoltheart commented 2 months ago

Can you write a sample failing test case showing the problem please?

DmitryMak commented 2 months ago

I'm not sure how I can write a test that controls the Culture of the runner. But to explain what I mean:

public class Sut {
    public String Format() {
        return 1_000.ToString("C", CultureInfo.CurrentCulture);
    }
}

[Subject(typeof(Sut))]
public class when_formatting {
    Establish context = () =>
        _subject = new();

    Because of = () =>
        _formatted = _subject.Format();

    // will fail if runner is not en-US, or OS regional settings use different currency symbol

    It should_format_using_current_culture = () =>
        _formatted.ShouldEqual("$1,000.00");   

    static Sut _subject;
    static String _formatted;
}
robertcoltheart commented 2 months ago

I don't think this is anything to do with the runner or the Should handlers. You should be able to control the culture yourself. For example, the below tests will pass using your example:

public class Sut
{
    public String Format()
    {
        return 1_000.ToString("C", CultureInfo.CurrentCulture);
    }
}

[Subject(typeof(Sut))]
public class when_formatting
{
    private Establish context = () =>
    {
        _subject = new();

        // Set the current culture for the tests
        CultureInfo.CurrentCulture = new CultureInfo("en-US");
    };

    Because of = () =>
        _formatted = _subject.Format();

    // will fail if runner is not en-US, or OS regional settings use different currency symbol

    It should_format_using_current_culture = () =>
        _formatted.ShouldEqual("$1,000.00");

    static Sut _subject;
    static String _formatted;
}
DmitryMak commented 2 months ago

Yes, we can control the culture, in fact this is what we do as a workaround. Not only do we need to set it explicitly, we also need to restore it (in the Cleanup after section). It adds noise to the spec and distracts from the actual code. I think this is the reason NUnit, for example, has this attribute

The SetCulture attribute is used to set the current Culture for the duration of a test. It may be specified at the level of a test, fixture or assembly. The culture remains set until the test or fixture completes and is then reset to its original value.

DmitryMak commented 2 months ago

The same reason why CulturedFact exists for xUnit

robertcoltheart commented 2 months ago

Have you looked into the lifecycle options for mspec? There is an example there that caters for culture-aware testing. See https://github.com/machine/machine.specifications/wiki/Advanced-Lifecycle-Options#iassemblycontext

DmitryMak commented 2 months ago

Thanks for sharing. I think IAssemblyContext is good for setting up a default culture. But sometimes the culture has to be specified at the individual spec level. A lot of other test frameworks implement this as an attribute.

robertcoltheart commented 2 months ago

Again, as above, you can do this via your Context. You can nest multiple spec classes within an outer context to reduce on copy+paste if that is the concern. I'm not keen on adding additional attributes or constructs for niche use cases such as this.

Closing as answered.