xUnit framework

Spread the love

In this tuorial I will explain what is the xUnit test framework, and how to implement it in our project; I will use a sample xunit testing c# .net 5 project.

Please subscribe to our YouTube channel and stay updated with our new released videos

What is xUnit

xUnit allows you to create new attributes to control your tests. It’s an open-source unit testing tool for .Net framework that’s compatible with ReSharper, CodeRush, and Xamarin. You can take advantage of xUnit to assert an exception type easily. Also, you can extend Fact or Theory attributes in xUnit and it provides excellent support for writing parameterized unit tests.

xUnit Installation

How to create a Unit test with xUnit

Add -> New Project → xUnit Test Project

Sample naming of the Unit tests project
Tests.ProjectName

To create a new unit test, right click on your Tests and create a new class, and call it (ValidityTest)

Note that xUnit supports two types of unit tests: [Fact] and [theory]

xUnit uses the [Fact] attribute to denote a parameterless unit test, which tests invariants in your code, while theories are tests that are true for a particular set of data passed as argument to the method.

In every unit method(for ex. SimpleTest), we have 3 sections (Arrange, Act and Assert). In the arrange section, we are telling the unit test what we are expecting, in the second part (Act), is the section where we inspect the data coming from the tested code ( a call to class, methods, libraries, etc ..), and in assert section we are executing a certain comparison between the expected and the actual data (Assert.Equal, Assert.True, Assert.Contain , etc…).

How to run a unit test

All the unit test that we create are present in the Test Explorer window (Test → Test Explorer)

You can run all the tests (Test → Run all tests), you can debug all tests (Test → Debug all tests), and you can run a single unit test by right clicking a specific test (in Test Explorer) and run it (or debug it)

Unit Test data feed

In order to feed the unit test with data, we need to use the [Theory] attribute instead of [Fact], there is 3 different ways to feed a Unit test with data.

  • [InlineData] – Pass the data for the theory test method parameters as arguments to the attribute
  • [ClassData] – Create a custom class that implements IEnumerable<object[]>, and use this to return the test data
  • [MemberData] – Create a static property or method that returns an IEnumerable<object[]> and use it to supply the test data.

[InlineData] Attribute


[InlineData] allows us to specify that a separate test is run on a particular Theory method for each instance of [InlineData]. We can have as many [InlineData] attributes on a given method.

In this example we are feeding the InlineDataTest arguments from data set in [InlineData] attribute.

[ClassData] Attribute

Create a custom class that implements IEnumerable<object[]>, and use this to return the test data.

In this example, the data is yielded from ClassDataTest to InlineDataTest.

[MemberData] Attribute

Create a static property or method that returns an IEnumerable<object[]> and use it to supply the test data

xUnit and Databases

These is many ways of dealing with databases in xUnit;

1 – The In-Memory database

2 – The Moq framework (Mocking database)

3 – Json data files

xUnit In-Memory database

The InMemory database can be used by unit tests to load specific tables to the memory and feed it up manually or automatically (using the Json files). The state of the InMemory database is dependent on the Unit test lifecycle, once the unit test finish its job, the correspondent resources are released.

xUnit In-Memory dependencies

Install InMemory package

xUnit In-Memory Implementation

Let’s take the following method that we need to test it;

public static async Task<bool> DataExecution(YodifemDb yodifem_context)
{
  //Some operations on the Devices table
  yodifem_context.select(x=>x.Devices).ToList .....
  ...
  
  Return true;
}

Example of Unit test (Data_ExecutionShouldWork)

    [Theory, ClassData(typeof(AllowedDevicesLookupData))]
        public void Data_ExecutionShouldWork(List<int> AllowedDevices)
        {
           var options = new DbContextOptionsBuilder<YodifemDb>()
           .UseInMemoryDatabase(databaseName: "YodifemDb")
           .Options;
            using (var context = new YodifemDb(options))
            {
                var dev_data = new Device { Id = 2567, Flags = 0, DevTypeId = 1, OrgId = 33, InfluxTagId = "2567" };
                context.Add<Device>(dev_data);
                context.SaveChanges();

                var act = DataExecution(context);
                Assert.True(act.Result.IsSuccess == true);
            }
        }

In this Unit Test, we load the YodiFemDB InMemory database, the tables of the database are all empty InMemory. The DataExecution method needs only Device table, so in the unit test we added some data to the Device table and called the tested method with the database sent as argument.

Moq Framework

Mocking Frameworks are used to create fake objects. We can stub, i.e., completely replace the body of member and function. It is used to isolate each dependency and help developers in performing unit testing in a concise, quick, and reliable way.

Moq framework can be used to fake the operations to a database; it is very similar to the work of InMemory database, but with Moq we can pretend we are working on a database, and execute different operations on a database without actually doing it. For that reason Moq method is more reliable than InMemory because we can save lot of resources, but in the other hand, InMemory is much more easier in implemention.

Moq Framework dependencies

Create Unit test with xUnit and C#

Moq Framework implementation

In this example we will be testing the same method that we have previously been testing with InMemory method.

#region Mock Database tests
        [Theory, ClassData(typeof(AuthTestData))]
       public void Data_ExecutionShouldWork(List<int> AllowedDevices)
        {
            //Mock database
            var devices = GetDevices().AsQueryable();

            var mockSet = new Mock<DbSet<Device>>();

            mockSet.As<IDbAsyncEnumerable<Device>>()
                  .Setup(m => m.GetAsyncEnumerator())
                  .Returns(new TestDbAsyncEnumerator<Device>(devices.GetEnumerator()));

            mockSet.As<IQueryable<Device>>()
                .Setup(m => m.Provider)
                .Returns(new TestDbAsyncQueryProvider<Device>(devices.Provider));

            mockSet.As<IQueryable<Device>>().Setup(m => m.Expression).Returns(devices.Expression);
            mockSet.As<IQueryable<Device>>().Setup(m => m.ElementType).Returns(devices.ElementType);
            mockSet.As<IQueryable<Device>>().Setup(m => m.GetEnumerator()).Returns(devices.GetEnumerator());
         
            var contextOptions = new DbContextOptions<YodifemDb>();
            var mockContext = new Mock<YodifemDb>(contextOptions);
            mockContext.Setup(c => c.Devices).Returns(mockSet.Object);

            var act = DataExecution(context);
            Assert.True(act.Count > 0);
        }


In this example we are doing the following.

Line 6: We are getting the devices data

Line 8: We are defining which tables we need to mock it

Line 10 – 20: We are executing different inner data conversion in order to iterate through this data later on when we pass the tables to the actual method that we are testing

Line 22 – 23: We are mocking the Database

Lin 24: Adding the mocked tables to the mocked database

Line 26: Passing the mocked database to the actual method we are testing

Line 27: executing different assertion operations


xUnit Json Files

In this paragraph, we created a custom xUnit theory test DataAttribute to load data from JSON files, that way you can export whole database to Json file, and use the Json data as a database.

The customized attribute is very dynamic, you can just specify which tables you need to use in your unit test, and it will load the data in your unit test.

JsonFileDataAttribute (Custom attribute)

 public class JsonFileDataAttribute : DataAttribute
    {
        private readonly string _filePath;
        private readonly string _propertyName1;
        private readonly string _propertyName2;
        private readonly string _propertyName3;
        private readonly string _propertyName4;
        private readonly string _propertyName5;
        private readonly string _propertyName6;

        private readonly string[] _propertyNameList;

        /// <summary>
        /// Load data from a JSON file as the data source for a theory
        /// </summary>
        /// <param name="filePath">The absolute or relative path to the JSON file to load</param>
        public JsonFileDataAttribute(string filePath)
            : this(filePath, null) { }

        /// <summary>
        /// Load data from a JSON file as the data source for a theory
        /// </summary>
        /// <param name="filePath">The absolute or relative path to the JSON file to load</param>
        /// <param name="propertyName">The name of the property on the JSON file that contains the data for the test</param>
        public JsonFileDataAttribute(string filePath, string propertyName1 = null, string propertyName2 = null, string propertyName3 = null, string propertyName4 = null, string propertyName5 = null, string propertyName6 = null)
        {
            _filePath = filePath;
            _propertyName1 = propertyName1;
            _propertyName2 = propertyName2;
            _propertyName3 = propertyName3;
            _propertyName3 = propertyName4;
            _propertyName3 = propertyName5;
            _propertyName3 = propertyName6;

            _propertyNameList = new string[] { _propertyName1, _propertyName2, _propertyName3, _propertyName4, _propertyName5, _propertyName6 };
        }

        /// <inheritDoc />
        public override IEnumerable<object[]> GetData(MethodInfo testMethod)
        {
            if (testMethod == null) { throw new ArgumentNullException(nameof(testMethod)); }

            // Get the absolute path to the JSON file
            var path = Path.IsPathRooted(_filePath)
                ? _filePath
                : Path.GetRelativePath(Directory.GetCurrentDirectory(), _filePath);

            if (!File.Exists(path))
            {
                throw new ArgumentException($"Could not find file at path: {path}");
            }

            // Load the file
            var fileData = File.ReadAllText(_filePath);

            var jsonData = JsonConvert.DeserializeObject<List<object[]>>(fileData);

            List<object[]> obj = new List<object[]>();

            List<object> list = new List<object>();

            object[] itm = new object[] { };

            jsonData.ForEach((item) =>
            {
                for (int i = 0; i < item.Length; i++)
                {
                    if (_propertyNameList.Contains(((Newtonsoft.Json.Linq.JProperty)JObject.Parse(item[i].ToString()).First).Name))
                        list.Add(item[i]);
                }
                obj.Add(list.ToArray());
            });

            return CastParamTypes(obj, testMethod);

        }

        /// <summary>
        /// Cast the objects read from the JSON file to the Type of the method parameters
        /// </summary>
        /// <param name="jsonData">Array of objects read from the JSON file</param>
        /// <param name="testMethod">Method Base currently test method</param>
        private IEnumerable<object[]> CastParamTypes(List<object[]> jsonData, MethodBase testMethod)
        {
            var result = new List<object[]>();
            var c = jsonData;
            // Get the parameters of current test method
            var parameters = testMethod.GetParameters();
            int j = 0;
            // Foreach tuple of parameters in the JSON data
            foreach (var paramsTuple in jsonData)
            {
                var paramValues = new object[paramsTuple.Length];
                // var paramValues = new object[jsonData.Count];

                // Foreach parameter in the method
                //for (int i = 0; i < paramValues.Length; i++)
                for (int i = 0; i < parameters.Length; i++)
                {
                    // Cast the value in the JSON data to match parameter type
                    var x = JObject.Parse(paramsTuple[i].ToString())[((Newtonsoft.Json.Linq.JProperty)JObject.Parse(paramsTuple[i].ToString()).First).Name];

                    paramValues[i] = CastParamValue(x, parameters[i].ParameterType);
                }

                result.Add(paramValues);
                j++;
            }
            return result;
        }

        /// <summary>
        /// Cast an object from JSON data to the type specifed
        /// </summary>
        /// <param name="value">Value to be casted</param>
        /// <param name="type">Target type of the cast</param>
        private object CastParamValue(object value, Type type)
        {
            // Cast objects
            if (value is JObject jObjectValue)
            {
                return jObjectValue.ToObject(type);
            }
            // Cast arrays
            else if (value is JArray jArrayValue)
            {
                return jArrayValue.ToObject(type);
            }
            // No cast for value types
            return value;
        }
    }


Json data file

[
    [
        {

            "Buildings": [
                {
                    "id": 1,
                    "name": "Main Building (YC)",
                    "description": "Syggrou",
                    "deployment_id": 1,
                    "org_id": 1,
                    "flags": 0
                },
                {
                    "id": 2,
                    "name": "Main Building (YC)",
                    "description": "Syggrou",
                    "deployment_id": 1,
                    "org_id": 1,
                    "flags": 0
                }
            ]
        },

        {
            "Organizations": [
                {
                    "id": 1,
                    "name": "Yodiwo Cyprus Org",
                    "description": "Multinational utility company",
                    "org_grp_id": 0,
                    "flags": 0
                },
                {
                    "id": 2,
                    "name": "Yodiwo Cyprus Org",
                    "description": "Multinational utility company",
                    "org_grp_id": 0,
                    "flags": 0
                }
            ]
        }

    ]
]



Unit test

public class AccessTests
    {
        [Theory]
        [JsonFileData("Data/AccessRightsData.json", "Buildings", "Organizations")]
        public void Access_UserHasReportingRights(List<Buildings> b, List<Organizations> o)
        {
            var result = o;
            var expected = b;
            Assert.Equal(expected.Count, result.Count);
       }


[JsonFileData(“Data/AccessRightsData.json”, “Buildings”, “Organizations”)] : In this line we are using the JsonFileData attribute, and specifying the path location of the json data file, we are also specifying which tables we want to use in our unit testing, here we will be using Buildings are Organizations.

public void Access_UserHasReportingRights(List<Buildings> b, List<Organizations> o) ; here we are telling the Unit test what we are expecting as parameters (List of building and Organizations)

https://www.rpchost.com