How to write testable code

The importance of testable code is not unknown to developers. There are lots of best practices for writing testable code. The main goal for us is to create a maintainable, extensible solution. I do not want to explain the benefits of testable code here. There are a lot of articles out there. In this article, I will try to list the most common practices for testable code.
Many developers think they write good code. But good-looking code is not always testable code. Let’s start!
Dependency Injection
Testable code is all about dependencies. The big benefit of using Inversion of Control / Dependency Injection via constructor parameter interfaces is that you easily can mock all those dependencies in your unit tests. Today's generation of software engineers uses dependency injection frameworks in their applications. Unfortunately, they still violate the inversion of control intentionally or unintentionally in some cases.
The new Keyword
The new keyword and direct object creation is the worst enemy of unit testing. In the previous example, I already showed how we can replace dependency injection with concrete object creation. Developers sometimes use new objections like this:
Static
One of the principles of a unit test is that it must have full control of the system under test. The best solution is to avoid static classes and methods in your code.
Private methods
Private methods are only an implementation detail whose existence and behavior are only justified by their use in public methods. In other words, once your public methods are tested, your private methods should be fully covered.
The best way to test a private method is via another public method. If this cannot be done and you need to test private methods separately then try out the following alternatives.:
1. Change the private method to the public
2. Use reflection.
3. Moving out responsibility
4. Extend testing to "internal" classes/methods
I personally prefer the 4th one because it is clean, Microsoft also uses this in their asp.net core repository
https://github.com/dotnet/aspnetcore/blob/master/src/Http/Routing/src/Matching/ILEmitTrieJumpTable.cs
In order to make the class more cleaner we can move the assembly block to our project file
File System
Static methods are not good for testability. But sometimes we need to use File static methods. Since these methods are not part of our application or come from third-party libraries, we can't change the static methods.
To avoid the testability issue we can create a file service and access the file through the service.
Database
To test the repository layer we need to mock the underlying EntityFrameworkCore layer. This means to mock EntityFrameworkCore classes, particularly entity context which is derived from DbContext and DbSet as a part of the DbContext.
Mocking database providers is difficult, cumbersome, and fragile. Luckily EF Core provides some solutions:
1: Production database system (LocalDB)
2: SQLite
3: The EF Core in-memory database
We recommend the EF in-memory database when unit testing something that uses DbContext. In this case, using the EF in-memory database is appropriate because the test is not dependent on database behaviour. Just don't do this to test actual database queries or updates.
But if the code is written this way, It will not be testable.
In this case, the new keyword should be removed as usual, but we don't need an interface for entity EF Core. We can directly mock the DbContext.
Third-Party Libraries
Third-party libraries most often become an obstacle while writing unit tests. So we have to use them with testability in our mind. Some of the third-party methods may contain a 'static' keyword. Though we can not remove the 'static' keyword, we can call the third-party static method in a non-static public method, extract an interface, and use that method in the system under test through dependency injection. But what to do about third-party libraries like Automapper?
Luckily, the auto mapper has a built-in interface for Mapper, so we do not need to create new abstractions. We can simply inject the interface and make the code testable.
Following these practices is not just good for unit tests but also improves the overall code design. It takes more time to write testable code but at the end of the day it really worth it.