0

Suppose we have a Calculator with a divide method throwing error if the denominator is 0:

    public class Calculator
    {
        public double Divide(int x, int y)
        {
            if (y == 0)
            {
                throw new ArgumentOutOfRangeException(paramName: nameof(y), message: CalculatorErrorMessages.DenominatorShouldNotBeZero);
            }

            return (double)x / y;
        }
    }
    public static class CalculatorErrorMessages
    {
        public static string DenominatorShouldNotBeZero = "Denominator should not be zero.";
    }

And here's the unit test which tries to test the raise of the exception:

    public class CalculatorUnitTest
    {
        [Fact]
        public void Test1()
        {
            var calculator = new Calculator();

            var ex = Assert.Throws<ArgumentOutOfRangeException>(() => calculator.Divide(1, 0));
            Assert.Contains(CalculatorErrorMessages.DenominatorShouldNotBeZero, ex.Message);
        }
    }

Stryker.NET reports the exception message survives mutation:

enter image description here

But that mutation survival is not an actual problem, since both the unit test code and the app code generate the exception message in the same way. I don't want to duplicate and maintain error messages in the app and the unit test.

For this, I could make Stryker ignore mutations on CalculatorErrorMessages but it does not sound like a good idea. Stryker will not catch a situation where the unit test does not handle error messages like it should. For example, let's say I add a new method DivideV2 in the Calculator which also throws an exception, and in the unit-test, instead of using the actual error message constant, I hard-code the expected error message:

        [Fact]
        public void Test2()
        {
            var calculator = new Calculator();

            var ex = Assert.Throws<ArgumentOutOfRangeException>(() => calculator.DivideV2(1, 0));
            Assert.Contains("Denominator should not be zero.", ex.Message);
        }

In this situation, the mutation survival should really not be ignored.

How can I make in such a way Stryker:

  • Does not show me 'false' mutations survivals
  • Still make my code unit tested by mutations
  • Not duplicate the error message strings across the app and unit-test
JamesL
  • 351
  • 2
  • 10
  • 1
    Why is your `static` field mutable in the first place? Why aren't you using `const String` or `static readonly String`? – Dai Jun 17 '23 at 07:39
  • 1
    _"I don't want to duplicate and maintain error messages in the app and the unit test."_ - well, yes, of course, otherwise that would be silly - but having tests for localizable human-readable error messages is also silly. Test for behaviour, not brittle things like localizable text (unless that specific text is _really important_, I guess). – Dai Jun 17 '23 at 07:41
  • @Dai I wrote the sample code in a rush, in a real app, you're right it should be a `const string`. I edited the code sample. – JamesL Jun 17 '23 at 07:50
  • @Dai I don't understand, why is testing for the exception message so silly? How else could I test to be sure the expected exception is actually raised? I don't want to create custom exception classes. – JamesL Jun 17 '23 at 07:53
  • 1
    _"How else could I test to be sure the expected exception is actually raised?"_ - simply checking that an `ArgumentOutOfRangeException` was thrown and that its `ActualValue == 0` is sufficient in this case. – Dai Jun 17 '23 at 07:58
  • @Dai Thanks, that will work for this specific case with exceptions. But will it work for any other scenarios with constants? The actual problem seem to be that the mutation just tries to mutate constants and expects units to fail, which for constants is not the case, because same constants are used by the unit test too. – JamesL Jun 17 '23 at 08:08
  • 1
    _"just tries to mutate constants"_ - that makes no sense: `const` in C#/.NET _cannot_ mutate. When you change the source string to `const` the Stryker test should not report any mutation (as `Exception.Message` is also immutable). Are you saying the test still gives the same results even with `public const string DenominatorShouldNotBeZero` ? – Dai Jun 17 '23 at 08:13
  • Oh! Now I see it! Using `const` makes an important difference. I was thinking Stryker is mutating `const` values. It does not. Facepalm! I guess I should read first about how mutating the unit-tests actually works. Thank you!! – JamesL Jun 17 '23 at 08:29
  • I edited the sample code back to using `static` so it makes sense for other readers. I could also delete the entire question since now I see it's not even well founded, I just didn't understand how the mutation in unit tests actually work. – JamesL Jun 17 '23 at 08:31
  • @Dai you want to provide an answer so I can marked as the answer? Otherwise, I can mark the one by Lars. – JamesL Jun 17 '23 at 17:33
  • If you want an answer, I recommend a song by UNKLE that I’m fond of: https://youtu.be/pC3vGOpCW9Y – Dai Jun 17 '23 at 17:51
  • @Dai I suck so much, I know :)) – JamesL Jun 18 '23 at 08:27

2 Answers2

1

Mutation vs Unit tests

To reiterate their purpose:

  • Unit tests are used to check your code for errors when changes to code are applied
  • Mutation tests are used to check if changes to your code, actually trip up the unit test. It does so by modifying the actual code and re-running tests, expecting them to fail.

The Stryker docs explain what mutations will be applied to the code.
In this case, a public static string YourVariable = "your-value"
would be change to public static string YourVariable = "", expecting your unit test to fail.

Calculator example:

You've expressed and identified couple of concerns:

I don't want to duplicate and maintain error messages in the app and the unit test.

This makes total sense to me. In my opinion moving in this direction would leave you with very brittle tests.

Ignore mutations on CalculatorErrorMessages but it does not sound like a good idea.

That's also correct, for the same reason you gave as an example. you want the mutation test to flag your code as properly tested when, in the unit test, you provide a hardcoded expectation on the error message.

your goals:

  • Stryker does not show me 'false' mutations survivals. Is caused by the test not failing when a mutation is done.
  • Still make my code unit tested by mutations. Is done by not ignoring stryker mutation on DenominatorShouldNotBeZero
  • Not duplicate the error message strings across the app and unit-test. Is done by writing the test like you did in the first example AND making sure mutations actually fail your unit tests.

That brings us to the crux, we have to make sure the unit-test fails after mutation (aka, killing the mutation). You could add additional tests & conditions that will trip after mutation.

What are we testing

we want the actual code:

  • To not throw exception and correctly calculate the division
  • To throw an exception when it's 0
  • The exception type to be of type ArgumentOutOfRangeException
  • The exception to use some constant as message
  • The message not to be empty
  • The exception to have paramName, (equal to the 2nd param name, or just not empty)

we want the unit test to test all of the above.

The mutation test is has correctly identified that a change on DenominatorShouldNotBeZero is currently not tripping the test. Changing the const to '', will still pass the unit-test.
It expects the unit test to fail when someone would change your const to ''.

I could assert that ex.Message is not an empty string.

Lars
  • 3,022
  • 1
  • 16
  • 28
1

So, long story short, one way to achieve your goals is to make the string const. Keep in mind that this way you are making use of the Stryker limitation described here. So different mutation testing framework or maybe some future Stryker version can still mark this as a surviving mutant.

Also, keep in mind that const is not "better" than static and replacing one with another can have unpleasant consequences. You shouldn't make it const just to calm Stryker down. In your case, I'd probably have it static readonly.

Ultimately, you need to define the level of testing for yourself, how black/white box you want it to be, how tautological you want it to be. And remember, you can ignore specific mutations.

psfinaki
  • 1,814
  • 15
  • 29