Ausnahme gefangen: SSL certificate problem: certificate is not yet valid 📌 Stop manually iterating over your test data in your unit tests! - Parametrized tests in C# using MSTest, NUnit, and XUnit

🏠 Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeiträge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden Überblick über die wichtigsten Aspekte der IT-Sicherheit in einer sich ständig verändernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch übersetzen, erst Englisch auswählen dann wieder Deutsch!

Google Android Playstore Download Button für Team IT Security



📚 Stop manually iterating over your test data in your unit tests! - Parametrized tests in C# using MSTest, NUnit, and XUnit


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

Code smell: manually iterating over test data

You probably have seen, or even coded yourself, a unit test that looks something like this:

[Test]
public void PokemonAreWaterType() 
{
    var pokemons = new List<string>() 
    {
        "Vaporeon",
        "Magikarp",
        "Squirtle"
    };

    foreach (var pokemon in pokemons) 
    {
        bool isWaterType = Pokedex.IsPokemonWaterType(pokemon);
        Assert.IsTrue(isWaterType, $"Pokemon '{pokemon}' should be water type.");
    }
}

You have a list of values that you want to test within the same test method, and you are looping over an array to perform the test. There are many problems with the code above:

  • While the above test is very simple and we can understand what is happening, this will rarely happen in real life.
  • We should also avoid nesting, the simpler the test, it will be easier to maintain.
  • If more than two test cases are failing, you will only get the results of one, as the test short circuits the first time an assert fails. Sure, you can save all the results in a results array, then assert on this array at the end, but you are again adding complexity to what should be a simple test.

Solution: parametrized testing

All the commonly used C# test frameworks support parametrized tests:

Data-driven tests, or parametrized tests, as you may wish to call them, take advantage of the test framework to simplify testing over a data source. This data source could be hardcoded values, collections, or any other data source.

We will create parametrized tests for all three common testing libraries in C#: MSTest, NUnit, and XUnit.

For the three test frameworks, we will showcase three unit test examples:

  • Unit test manually iterating over the test data.
  • Using attributes for each row of the test data.
  • Using a static array as the test data source.

The code used in this post can be found at: https://github.com/rogeliogamez92/blog-data-driven-testing

Example application: Pokémon Factory

We will create a Pokémon factory that takes two strings, name, and type, and tries to create a valid Pokémon. If the type is not supported by our application, it will create a Pokémon with an invalid type.

Pokémon class:

public class Pokemon
{
    public enum PokemonType
    {
        UnsupportedType,
        Grass,
        Fire,
        Water
    }

    public readonly string Name;
    public readonly PokemonType Type;

    internal Pokemon(string name, PokemonType type)
    {
        Name = name;
        Type = type;
    }
}

Pokémon factory class:

public class PokemonFactory
{
    public Pokemon CreatePokemon(string name, string type)
    {
        if (!Enum.TryParse(type, ignoreCase: true, out Pokemon.PokemonType pokemonType))
        {
            pokemonType = Pokemon.PokemonType.UnsupportedType;
        }

        return new Pokemon(name, pokemonType);
    }
}

While this is not a true implementation of the factory method pattern, if you want to know more about it, I strongly recommend you to read: https://refactoring.guru/design-patterns/factory-method.

Parametrized tests in MSTest

Let's start manually iterating over the test data:

[TestMethod]
public void IteratingOverData()
{
    /// Tuple(PokemonName, PokemonType, ExpectedPokemonType
    var pokemonData = new List<Tuple<string, string, Pokemon.PokemonType>>()
    {
        new ("Bulbasaur", "Grass", Pokemon.PokemonType.Grass),
        new ("Charmander", "Fire", Pokemon.PokemonType.Fire),
        new ("Squirtle", "Water", Pokemon.PokemonType.Water),
        new ("ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType),
    };

    var pokemonFactory = new PokemonFactory();

    foreach (var pd in pokemonData)
    {
        var pokemon = pokemonFactory.CreatePokemon(pd.Item1, pd.Item2);
        Assert.AreEqual(pokemon.Type, pd.Item3);
    }
}

We have our data in an array that we iterate and assert on each item. It works, but there is something off.

Let's see the same test using the DataRow attribute from MSTest:

[TestMethod]
[DataRow("Bulbasaur", "Grass", Pokemon.PokemonType.Grass)]
[DataRow("Charmander", "Fire", Pokemon.PokemonType.Fire)]
[DataRow("Squirtle", "Water", Pokemon.PokemonType.Water)]
[DataRow("ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType)]
public void UsingDataRow(string pokemonName, string pokemonType, Pokemon.PokemonType expectedPokemonType)
{
    var pokemonFactory = new PokemonFactory();

    var pokemon = pokemonFactory.CreatePokemon(pokemonName, pokemonType);

    Assert.AreEqual(pokemon.Type, expectedPokemonType);
}

At first glance, we can see an aesthetic improvement. That ugly foreach is no more.

Now, what happens when there are errors in the Fire and Water test cases? We will append "Error" to both input strings for type.

[TestMethod]
public void IteratingOverData()
{
    /// Tuple(PokemonName, PokemonType, ExpectedPokemonType
    var pokemonData = new List<Tuple<string, string, Pokemon.PokemonType>>()
    {
        new ("Bulbasaur", "Grass", Pokemon.PokemonType.Grass),
        new ("Charmander", "FireError", Pokemon.PokemonType.Fire), // ERROR
        new ("Squirtle", "WaterError", Pokemon.PokemonType.Water), // ERROR
        new ("ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType),
    };

    ///...
[TestMethod]
[DataRow("Bulbasaur", "Grass", Pokemon.PokemonType.Grass)]
[DataRow("Charmander", "FireError", Pokemon.PokemonType.Fire)] // ERROR
[DataRow("Squirtle", "WaterError", Pokemon.PokemonType.Water)] // ERROR
[DataRow("ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType)]
public void UsingDataRow(string pokemonName, string pokemonType, Pokemon.PokemonType expectedPokemonType)
{
    ///...

Let's see the difference in Visual Studio 2022 Test Explorer:

Visual Studio 2022 Test Explorer results

Using DataRow is so much better! If we manually iterate we would need to jump through hoops in code to try to give a similar report as the method using DataRow.

What if we want to reuse the test data? In MSTest we first declare the test data array, and reference it using the DynamicData attribute:

public static IEnumerable<object[]> PokemonData => new[]
{
    new object[] { "Bulbasaur", "Grass", Pokemon.PokemonType.Grass },
    new object[] { "Charmander", "Fire", Pokemon.PokemonType.Fire },
    new object[] { "Squirtle", "Water", Pokemon.PokemonType.Water },
    new object[] { "ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType },
};

[TestMethod]
[DynamicData(nameof(PokemonData))]
public void UsingDynamicData(string pokemonName, string pokemonType, Pokemon.PokemonType expectedPokemonType)
{
    var pokemonFactory = new PokemonFactory();

    var pokemon = pokemonFactory.CreatePokemon(pokemonName, pokemonType);

    Assert.AreEqual(expectedPokemonType, pokemon.Type);
}

Now we can reference that same array in as many tests as we want.

Parametrized tests in NUnit

For NUnit, we use the TestCase attribute instead of DataRow:

[TestCase("Bulbasaur", "Grass", Pokemon.PokemonType.Grass)]
[TestCase("Charmander", "Fire", Pokemon.PokemonType.Fire)]
[TestCase("Squirtle", "Water", Pokemon.PokemonType.Water)]
[TestCase("ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType)]
public void UsingTestCase(string pokemonName, string pokemonType, Pokemon.PokemonType expectedPokemonType)
{
    var pokemonFactory = new PokemonFactory();

    var pokemon = pokemonFactory.CreatePokemon(pokemonName, pokemonType);

    Assert.AreEqual(expectedPokemonType, pokemon.Type);
}

And if we want to use an array, we use the TestCaseSource:

public static IEnumerable<object[]> PokemonData => new[]
{
    new object[] { "Bulbasaur", "Grass", Pokemon.PokemonType.Grass },
    new object[] { "Charmander", "Fire", Pokemon.PokemonType.Fire },
    new object[] { "Squirtle", "Water", Pokemon.PokemonType.Water },
    new object[] { "ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType },
};

[TestCaseSource(nameof(PokemonData))]
public void UsingTestCaseSource(string pokemonName, string pokemonType, Pokemon.PokemonType expectedPokemonType)
{
    var pokemonFactory = new PokemonFactory();

    var pokemon = pokemonFactory.CreatePokemon(pokemonName, pokemonType);

    Assert.AreEqual(expectedPokemonType, pokemon.Type);
}

Parametrized tests in XUnit

For XUnit, for parametrized tests, we use the Theory attribute instead of Fact.

Instead of DataRow we use the InlineData attribute:

[Theory]
[InlineData("Bulbasaur", "Grass", Pokemon.PokemonType.Grass)]
[InlineData("Charmander", "Fire", Pokemon.PokemonType.Fire)]
[InlineData("Squirtle", "Water", Pokemon.PokemonType.Water)]
[InlineData("ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType)]
public void UsingInlineData(string pokemonName, string pokemonType, Pokemon.PokemonType expectedPokemonType)
{
    var pokemonFactory = new PokemonFactory();

    var pokemon = pokemonFactory.CreatePokemon(pokemonName, pokemonType);

    Assert.Equal(expectedPokemonType, pokemon.Type);
}

And for a data array, the MemberData attribute:

public static IEnumerable<object[]> PokemonData => new[]
{
    new object[] { "Bulbasaur", "Grass", Pokemon.PokemonType.Grass },
    new object[] { "Charmander", "Fire", Pokemon.PokemonType.Fire },
    new object[] { "Squirtle", "Water", Pokemon.PokemonType.Water },
    new object[] { "ProfessorOak", "Human", Pokemon.PokemonType.UnsupportedType },
};

[Theory]
[MemberData(nameof(PokemonData))]
public void UsingMemberData(string pokemonName, string pokemonType, Pokemon.PokemonType expectedPokemonType)
{
    var pokemonFactory = new PokemonFactory();

    var pokemon = pokemonFactory.CreatePokemon(pokemonName, pokemonType);

    Assert.Equal(expectedPokemonType, pokemon.Type);
}

Final thoughts

In this post, we went over all the three most common C# test frameworks and showed how we can write parametrized tests to avoid manually iterating over the test data.

Do not stop with just what I showed you today. It is cool being able to share the same test data array between several tests, but these frameworks are very complete. The three of them support other types of data sources such as files and services. I encourage you to go to their site and read the documentation of your framework of choice:

You can find the code of this post at: https://github.com/rogeliogamez92/blog-data-driven-testing

...



📌 Make NUnit Xamarin Better for Unit Testing! | MonkeyFest USA 2020


📈 44.74 Punkte

📌 MSTest Runner from Microsoft makes tests faster and portable


📈 37.59 Punkte

📌 Writing tests with MSTest v2


📈 35.81 Punkte

📌 Writing tests with MSTest v2 | On .NET


📈 35.81 Punkte

📌 Failing Faster and Iterating with Modern Software Development Practices


📈 32.51 Punkte

📌 Stop Hardcoding Your Unit Tests!


📈 32.13 Punkte

📌 Medium CVE-2020-2115: Jenkins Nunit


📈 31.81 Punkte

📌 CVE-2022-43414 | NUnit Plugin up to 0.27 on Jenkins Agent-to-Controller Message protection mechanism


📈 31.81 Punkte

📌 Testing with the xUnit Framework - Theories and Assertions (3 of 12) | Automated Software Testing


📈 30.91 Punkte

📌 Iterating Products with User Feedback - NA GDE Summit


📈 30.72 Punkte

📌 Iterating through Arrays: forEach vs for...of


📈 30.72 Punkte

📌 Iterating on Web Handlers Implementation


📈 30.72 Punkte

📌 Find Hidden Info using Google Dorking manually, and Automated using Pagodo


📈 29.73 Punkte

📌 CVE-2022-34181 | xUnit Plugin up to 3.0.8 on Jenkins Agent-to-Controller Message protection mechanism


📈 29.12 Punkte

📌 Testing with the xUnit Framework - More Assertions (4 of 12) | Automated Software Testing


📈 29.12 Punkte

📌 Testing with the xUnit Framework - Overview (2 of 12) | Automated Software Testing


📈 29.12 Punkte

📌 Harnessing Class Fixtures in xUnit


📈 29.12 Punkte

📌 XUnit In ASP.NET Core – What You Need To Know To Start


📈 29.12 Punkte

📌 JUnit Tests in Java: A Guide to Writing Effective Unit Tests


📈 28.63 Punkte

📌 How to start and stop services manually on Windows 10


📈 28.38 Punkte

📌 CVE-2023-24441 | MSTest Plugin up to 1.0.0 on Jenkins XML Parser xml external entity reference


📈 27.96 Punkte

📌 Schlanke Alternative zu VSTest: Microsoft veröffentlicht neuen MSTest Runner


📈 27.96 Punkte

📌 Schlanke Alternative zu VSTest: Microsoft veröffentlicht neuen MSTest Runner


📈 27.96 Punkte

📌 Der neue MSTest Runner


📈 27.96 Punkte

📌 Native AOT-Anwendungen mit MSTest testen


📈 27.96 Punkte

📌 Quickly Evaluate your RAG Without Manually Labeling Test Data


📈 27.21 Punkte

📌 How to Test Your Unit Tests


📈 26.93 Punkte

📌 Stop creating HTTP clients manually - Part I


📈 26.59 Punkte

📌 Stop Manually Coding UI Components! 🔼❌


📈 26.59 Punkte

📌 How to Confidently Write Unit Tests using React Testing Library


📈 25.9 Punkte

📌 C++20: Module Interface Unit und Module Implementation Unit


📈 25.87 Punkte

📌 Stop requiring only one assertion per unit test: Multiple assertions are fine


📈 25.49 Punkte











matomo