top of page

Subscribe to get best practices, tutorials, and many other cool things directly to your email inbox

  • Writer's pictureAhmed Tarek

How To Fully Cover I/O File Based Applications in .NET C# With Unit Tests

Updated: Apr 17

Learn how to divide the application into smaller modules which you can cover by 100%


How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Mr Cup / Fabien Barral on Unsplash, adjusted by Ahmed Tarek

While working for different software houses, in more than one occasion I had the chance to work on an application which is mainly based on I/O File operations.


The biggest challenge the team was facing while working on such kind of applications is that the I/O File operations are so hard to be covered by unit tests, automate Bamboo builds, and many other things.


Therefore, once and for all, I decided to come up with the best design I can come up with to overcome these challenges. However, just as a reminder, nothing in software is an absolute truth. For every application you have to reconsider your design, see where it fits, where it doesn’t fit, and finally you need to adapt.


Now, as usual, I am going to provide a simple example and walk you through the trip to come up with the best -possible- solution.


 

Example Application


How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering

Our application is so simple in terms of requirements:


👉 The UI is simple as you can see. For simplicity, it is implemented as a Windows Forms project.


👉 The Data file the application deals with is a text file with the extension .zzz


👉 Every entry in the data file is in the form of {name},{age},{profession} as follows:

Mohamed,20,Accountant
Patrick,26,Mechanical Engineer
Sara,23,Software Tester

👉 Note that the entries are separated by the new line character \r\n.


👉 Click the Browse button to open a .zzz file. The path of the file would appear in the read-only text box above the Browse button.


👉 Click the Get All button so that the application reads that data from the selected .zzz file and present them into the Reach Text Box at the bottom of the UI.


👉 Click the Add button so that the application adds a hardcoded entry to the file and updates the Reach Text Box at the bottom of the UI.


👉 Click the Remove button so that the application removes the last entry in the file and updates the Reach Text Box at the bottom of the UI.


👉 Here are some screenshots to help you get the big picture:


How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
After clicking Browse and selecting a .zzz file

How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
After clicking Get All

How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
After clicking Add

How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
After clicking Remove

 


 

How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Mikael Seegen on Unsplash

Disclaimer

  1. Some best practices have been dropped/ignored to drive the main focus to the core purpose and best practices of this article.

  2. Some enhancements could be done on the solution but they would be left for you to implement as an exercise.

  3. All the code could be found on this repository so that you can easily follow.

  4. A sample Data file is also available on the same repository and you can find it here.


How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Mehdi on Unsplash, adjusted by Ahmed Tarek

Bad Code


This might be the first thing comes to your mind when trying to implement this application.



What we can notice here is that all the code is in one place:

  1. The logic of dealing (opening, reading content, and writing content) with a physical file.

  2. The logic of executing UI commands.

  3. The logic of formatting data and updating UI.


This creates many challenges like:

  1. Too many responsibilities for one class.

  2. Depending on static classes like System.IO.File.

  3. Can’t test I/O operations logic without getting UI logic into your way.

  4. Can’t test UI logic without getting I/O operations logic into your way.

  5. Will need to always have physical data files to be able to cover the code with unit tests.

  6. Even if you succeeded into creating these unit tests and their related physical files, these files would always require maintenance, storage,

  7. And they would make planning and implementing Continuous Integration (CI) and Continuous Delivery/Deployment (CD) a nightmare.


Therefore, now it is time for the way of fixing this.


 


 

How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Carson Masterson on Unsplash, adjusted by Ahmed Tarek

Good Code


The main idea here is to divide the whole solution into smaller parts which we can control and easily cover with unit tests.



ISystemFileOperationsManager



What we can notice here is:

  1. This is an interface representing some the I/O File operations we use in our whole solution.

  2. The main goal of having this interface is to abstract the dependency we have on the I/O File operations.

  3. This abstraction would be so helpful when trying to cover our solution with unit tests as now we have a defined dependency which we can mock.



NtfsOperationsManager



What we can notice here is:

  1. This is implementing the ISystemFileOperationsManager interface.

  2. It is a thin wrapper to System.IO.File class.

  3. That’s why we can easily and safely exclude this class from code coverage as we actually don’t cover .NET built-in classes.



IDataFileRepository



What we can notice here is:

  1. This is the interface representing the repository manager which knows about the existence of our Data files and how to write and read text to and from them.

  2. This abstraction would be so helpful when trying to cover our solution with unit tests as now we have a defined dependency which we can mock.



DataFileRepository



What we can notice here is:

  1. This is implementing the IDataFileRepository interface.

  2. It internally depends on the ISystemFileOperationsManager and uses it to do the I/O File operations.



DataEntry



What we can notice here is:

  1. This is the data object representing our entity that is saved and retrieved to and from our Data files.

  2. The Age property here is implemented as string for simplicity.

  3. Also, this class should implement IEquatable<DataEntry> to make it easy to apply comparison operations on it. I would leave this part for you to implement.


 


 

IDataTransformer



What we can notice here is:

  1. This is the interface representing any transformer which has the knowledge of how to convert between text and our DataEntry.

  2. This abstraction would be so helpful when trying to cover our solution with unit tests as now we have a defined dependency which we can mock.



DataTransformer



What we can notice here is:

  1. This is implementing the IDataTransformer interface.

  2. This class encapsulates all the knowledge about our Data transformation between text and DataEntry.



IDataManager



What we can notice here is:

  1. This is the interface representing any manager which is capable of managing our application data without any knowledge about the media where this data is saved in.

  2. On this level, there is no reference for File.



DataManager



What we can notice here is:

  1. This is implementing the IDataManager interface.

  2. It internally depends on IDataFileRepository and uses it to persist and retrieve data in and from Data files.

  3. Also, it internally depends on IDataTransformer and uses it to perform the required conversions.


 


 

MainApplication



What we can notice here is:

  1. This is the class which handles the business logic triggered through the application UI.

  2. I didn’t abstract this class as an interface but for sure you can do it. I will leave this for you to implement.



FrmMain



What we can notice here is:

  1. This is the main Form class.

  2. It internally depends on MainApplication class and uses it to execute the main business logic of the application.


 


 

How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by Testalize.me on Unsplash, adjusted by Ahmed Tarek

Time For Testing


Now, it is time for trying to cover our solution with unit tests. What you would notice here is how easy it would be to cover our whole solution with unit tests.


Every module is now designed to do as little as possible and has its own dependencies well defined.


So, now let’s create our unit tests project. I am using NUnit and Moq libraries for testing and Mocking.



DataFileRepositoryTests




DataManagerTests



 


 

DataTransformerTests




MainApplicationTests



 

When we run all these unit tests and calculate the test coverage, this would be the result.


How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering

As you can notice from the screenshot, the only missing part from the coverage is the Form code itself. Could it be covered?


Yes, it could be covered as well, however, I will leave this for you to implement.


How to achieve 100% coverage of I/O file based applications in DotNet (.NET) CSharp (C#) Test Driven Development (TDD). Best Practice Code Coding Programming Software Development Architecture Engineering
Photo by nck_gsl on Pixabay, adjusted by Ahmed Tarek

Final Thoughts


Now, with the new design we can easily cover every aspect of our solution with unit tests and it is so easy to have full control over our application modules. That’s it…


Finally, hope you found reading this article as interesting as I found writing it.



Recent Posts

See All

Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating

Subscribe to get best practices, tutorials, and many other cool things directly to your email inbox

bottom of page