Home > Enterprise >  How to write NUnit-Tests for LinQ-Queries?
How to write NUnit-Tests for LinQ-Queries?

Time:03-30

For exercise purposes I got a big .csv file containing multiple columns for Persons. The exercise is to write multiple LinQ Queries, for example choosing only Person that are female etc. For example:

var query = persons.Where(p => p.Salutation == "Ms.");

To apply queries, I first converted each Row of the .csv file into a class Person and added the Object to a List containing of Persons.

        List<Person> persons = new List<Person>();
        var cr = new CsvableBase.CsvReader<Person>();
        var csvPeople = cr.Read("data.csv", headers = true);

        foreach (var person in csvPeople)
        {
            persons.Add(person);
        }
    }

This works as intended, and I can queck each query by looking at the console. For example:

    public static void GetOnlyFemale(List<Person> persons)
    {
        var query = persons.Where(p => p.Salutation == "Ms.");
        foreach (var person in query)
        {
            var properties = person.GetType().GetProperties();
            foreach (var property in properties)
            {
                Console.Write($"{property.Name}: {property.GetValue(entry)}"   ",");
            }

            Console.WriteLine();
        }
    }

Now, the next exercise is to write Unit-Tests for each query and I got no clue how to tackle that. I though about creating some rows that show the correct result and compare them against the same amount of rows of the query. But there must be a better way?

CodePudding user response:

The reason that you have problems writing a unit test, is because you don't separate your concerns: your class can do too much. Make smaller classes that have only one task. If you are not familiar with separation of concerns, consider to read some background information about it.

Separate your classes into a class that represents your storage mechanism, which in your current version is a CSV file, and a class that does the queries on your storage mechanism.

Hide in the interface of your storage mechanism that it is a CSV file. This way, in future you can change the storage format into a JSON file, or XML, a database, or maybe in future you fetch your data from the internet. In fact, for your queries it is not really important where and how your data is stored. All you really want to know, is that you can retrieve an enumerable sequence of similar objects for this.

In your case, your storage contains a sequence of Persons. So your storage should at least have an interface like this:

Interface IMyDataFetcher
{
    IEnumerable<Person> Persons {get;}
    ... // fetch other data you store in your storage
}

Quite often this storage is called a repository, in the sense of a warehouse where you store items in and later can fetch them unchanged. The class that fetches data from your CSV file will be like this:

class MyCsvRepository : IMyDataFetcher  // TODO: invent a proper name
{
    public string FileName {get; set;}

    public IEnumerable<Person> Persons
    {
        get {...}
    }
}

In the get you open the CSV file and read the lines one by one, to return the persons. If needed you can be smart and remember the read lines, so next time you want Persons, you don't need to read the file again, but that is out of scope of this question

As your repository class doesn't have a lot of functionality, it is quite easy to write unit tests for this, especially tests for file not found, empty file, file with only one Person, file with other records than Persons, etc.

The class that does the queries, is separated from the storage. It only knows, that somehow you can get a sequence of Persons:

class MyPersonSelector
{
    public IMyDataFetcher Storage {get; set;}

    public IEnumerable<Person> Females
    {
        get => this.Storage.Where(person => person.Gender == Gender.Female);
    }

    public IEnumerable<Person> Adults
    {
        get => this.Storage.Where(person => person.Age > 21);
    }

    // etc.
}

For your unit test, you don't need the CSV file, you just make it a smart list.

For example:

Requirement 1: If the storage contains only males, property Females should return an empty sequence.

Unit test:

IEnumerable<Person> malesOnlyStorage = new List<Person>()
{
    new Person() {Gender = Gender.Male, ...},
    new Person() {Gender = Gender.Male, ...},
    new Person() {Gender = Gender.Male, ...},
}

MyPersonSelector testObject = new MyPersonSelector
{
    Storage = malesOnlyStorage,
};

IEnumerable<Person> fetchedFemales = testObject.Females;

// fetchedFemales should be empty
Assert.IsFalse(fetchedFemales.Any());  // this depends on the test suite you use

Summary

By separating your concerns, you have smaller classes, that have only one task. Smaller classes have fewer functions, and thus smaller unit tests.

By separating the storage from the queries on the storage, your software supports any kind of storage. Therefore for your unit tests you can use simple lists.

Your software will be ready for future changes: if you also need to support sequences of Persons from an XML file, or from a database, your queries will still work. If you need to add another query, your storage class won't have to change, and thus the unit test for your storage class doesn't have to change.

  • Related