DEV Community

zakwillis
zakwillis

Posted on

Novel tips on unit testing - avoiding hard coding

Unit testing is all about mocks and fakes?

When I first saw a mock, I didn't have a clue what it was for. Once I understood what they were for, then I was more confused;

  1. Why would somebody hard code values into a stub class when it is likely that data doesn't have any business meaning?
  2. Why spend so much time creating stubs?
  3. We know that unit testing is designed to avoid external dependencies, but in doing this, we end up with a load of hard coded values not visible to functional testers.
  4. Unit tests, are esoteric by nature.

Fundamentally, it seemed that far too much time was spent hand crafting imaginary data which would later need changing as more business knowledge became apparent.

Making unit tests more useful

Please try to avoid getting upset about what a unit test is versus what an integration test is. Those who are particularly precise on definitions won't be too happy.

Using features of NUnit

Using categories

Many may not know, we can categorise tests to group them. I tend to name them something like;

  1. Unit Test.
  2. Ligthweight Test.
  3. Integration Test. Some may not know that the Test Explorer Window lets your run suites of tests. More useful is a test runner such as NUnit Test Runner will let you run a specific suite of tests. This is great for Continuous Integration.
[Category("Integration Test")]

Using test arguments as asserts

This is particularly useful. This shows developers what we expect the result to be.

[Category("Unit Test")]
        [TestCase("£", "$", nameof(TokenBoundaryCharacterType.BothDifferent))]
        [TestCase("£", "", nameof(TokenBoundaryCharacterType.StartOnly))]
        [TestCase("", "£", nameof(TokenBoundaryCharacterType.EndOnly))]
        [TestCase("£", "£", nameof(TokenBoundaryCharacterType.SameStartAndEnd))]
        [TestCase("", "", nameof(TokenBoundaryCharacterType.CantClassify))]
        public void TestBoundaryStartEndsAreCorrect(string Start, String End, string TokenBoundaryCharacterType)
        {
            IRuleRetriever tokenBoundaryDetector = serviceProvider.GetRequiredService<IRuleRetriever>();

            var tokenBoundaryCharacterType = TokenBoundaryCharacterType.GetEnumFromText<TokenBoundaryCharacterType>();

            var result = tokenBoundaryDetector.DetermineTokenBoundaryCharacterType(new typTokenBoundary()
            { StartCharacter = Start, EndCharacter = End });

            Assert.AreEqual(tokenBoundaryCharacterType, result);
        }

Test Cases

        [TestCase("", "", nameof(TokenBoundaryCharacterType.CantClassify))]
        public void TestBoundaryStartEndsAreCorrect(string Start, String End, string TokenBoundaryCharacterType)

You will have seen this earlier, but by using Test Cases, you can run the same code multiple times for different conditions.

Using Serialisation and Deserialisation (UK not American spelling).

One of the most important features I find with creating useful tests, is holding test data outside of the application altogether. People will claim this is creating external dependencies but who cares?

It goes something like this.
1). Store data in a database table, or some other structure.
2). Retrieve that data and serialise it.
3). Save it somewhere on disk, probably a test folder.
4). Whenever you run a test, access that file instead.

We may ask, why would we do this? We risk tests failing if the file isn't there, or the data isn't what was expected. Here are some major advantages as to why we should start to think about serialising and deserialising objects;

  • By working with testers, they may be able to place files in a directory to test their conditions. i.e. They can produce the test cases, export them as json.
  • Applications may be able to produce data for testing.
  • Reduced hard coding.
  • More visibility/Less blackbox testing.

Serialisation/Deserialisation example test

This is not complete and some code has been omitted. I used FakeItEasy, NewtonSoft and NUnit to do these tests. You will note I don't have any asserts because it is more to do with testing concepts than asserting.

namespace IRTest.PublisherTests
{

    public class ExportSetting
        {
        public string FolderPath { get; set; }
        public string FileName { get; set; }
        public string ExportName { get; set; }

    }

    [TestFixture]
    public class TestWebsitePublication
    {
        private IServerConfig serverConfig { get; set; }

        public ExportSetting exportSetting { get; set; }

        private ServiceProvider serviceProvider { get; set; }

        [SetUp]
        public void SetUp()
        {
            var services = new ServiceCollection();

            SetupSetting();

            services.AddSingleton<IServerConfig>(serverConfig);

            services.AddTransient<IGetActivePackagePublication, GetActivePackagePublication>();

            services.AddTransient<IGetActivePackagePublicationEndPoint, GetActivePackagePublicationEndPoint>();

            services.AddTransient<IServiceGetReportsAndPublications, ServiceGetReportsAndPublications>();

            services.AddTransient<ISimpleFileCache, SimpleFileCache>();

            serviceProvider = services.BuildServiceProvider();

        }

        public void SetupSetting()
        {
            var config = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .Build();

            var setting = new ServerConfig()
            {
                ConnectionString = config.GetConnectionString("PublishWebDB")
            };

            this.serverConfig = setting;

            this.exportSetting = new ExportSetting()
            {
                ExportName = "Cache"
                 , FileName = "AllPublicationAndReports.json"
                 , FolderPath = @"C:\InfoRhino\Test\Publication\"
            };

        }

        [Test]
        [Category("Integration Test")]
        public void OutputStaticDataSet()
        {

            PublishGetPublicationAndReports();

        }


        public void PublishGetPublicationAndReports()
        {
            var datarepo = serviceProvider.GetRequiredService<IServiceGetReportsAndPublications>();

            var data = datarepo.GetAllPublicationAndReports(DateTime.Now);

            var cacheWriter = serviceProvider.GetRequiredService<ISimpleFileCache>();

            cacheWriter.WriteObjectToFile(data, exportSetting.FolderPath, exportSetting.FileName, exportSetting.ExportName);
        }

        public IEnumerable<PublicationAndReports> GetPublicationAndReports()
        {

            var datarepo = serviceProvider.GetRequiredService<IServiceGetReportsAndPublications>();

            var cacheWriter = serviceProvider.GetRequiredService<ISimpleFileCache>();

            var fullFile = $"{exportSetting.FolderPath}\\{exportSetting.ExportName}\\{exportSetting.FileName}";

            if (!(System.IO.File.Exists(fullFile)))
            {
                PublishGetPublicationAndReports();
            }

            var data = cacheWriter.ReadObjectFromFile<IEnumerable<PublicationAndReports>>(
                $"{exportSetting.FolderPath}\\{exportSetting.ExportName}", exportSetting.FileName);

            return data;

        }

        public IDistributedCache GetCacheItem(byte[] textFromData)
        {


            var distCache = A.Fake<IDistributedCache>();

            A.CallTo(() => distCache.Get("Hello"))
                .Returns(textFromData);

            return distCache;
        }


        [Test]
        public void CheckData()
        {

            var data = GetPublicationAndReports();

            var textFromData = System.Text.UTF8Encoding.Unicode.GetBytes( data.First().Serialise());

            var cache = GetCacheItem(textFromData);

            var returnedText = cache.Get("Hello");
        }

Explaining the code

There isn't too much to it. We fake the cache, stating what data we will return if a get method is returned. FakeItEasy won't work with Extension Methods hence me not using the GetString method.

I have an external library for repository work and a helper for serialising and deserialising to and from disk.
The Setup method is the Microsoft Dependency Injection approach in .Net Core. I am not the biggest fan of this, but it works.
There is some kind of nasty string nonsense, but it can be tidied up.
The CheckData method can/could have an assert I guess.

For me, this is enough to let me see test data outside an application without relying upon a database table.

The more aware will realise, a key advantage of this is we can persist data from databases at certain points to file and then no longer have to worry about the database.

Improving the quality

  • Well, I would probably put a wrapper around the repository/data access layer to use a cache data source.
  • Asserts are a must.
  • More code coverage of the IDistributedCache contract.
  • Move hard coding into the appsettings.json file. Again, this is not nice right now.
   public void SetupSetting()
        {
            var config = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .Build();

            var setting = new ServerConfig()
            {
                ConnectionString = config.GetConnectionString("PublishWebDB")
            };

            this.serverConfig = setting;

            this.exportSetting = new ExportSetting()
            {
                ExportName = "Cache"
                 , FileName = "AllPublicationAndReports.json"
                 , FolderPath = @"C:\InfoRhino\Test\Publication\"
            };

        }

Conclusion

I may do a follow up with a more realistic example if I need to write the code but hopefully it may help you approach unit testing in a different way. I think most people will think this is a bad approach, but I am a data person which means I want to be able to see as much as I can outside of code libraries.

Top comments (0)