✅ Testing Guidance (Unit/Integration)
Hello! I am having the worst time trying to wrap my head around how/what/when to do unit tests vs integration tests. My company currently has no testing setup and so I am being tasked with adding it to one of our web apis we are currently rewriting.
The part I am mainly struggling on is where the db comes into play. We have an IUnitOfWork interface that's currently only exposing
Set<T> Query<T> (which is just Set<T> with AsNoTracking enabled) and SaveChanges on the DbContext. That's injected pretty much everywhere we need to make a database query, so most of our services have it injected. This was a pattern set before I joined the company and we've just followed it since.
With that said, if I wanted to unit test one of those services that have a dependency on IUnitOfWork, it doesn't seem like I could easily mock this since it's generic and reliant on ef core. What should I do in this scenario? Just integration test it? Inject the concrete DbContext and try and mock the DbSets? Abstract away the "repository" (that seems overkill just to have testing) using the repository pattern on top of ef core?
The loose definitions of the different testing approaches has been driving me insane while trying to figure out how to set all this up. Would I hybrid approach work or is it not recommended? For example if I wanted to test a particular service that is dependent on the db should I spin up the db in a test container and run the test? From what I can tell the integration tests use WebApplicationFactory to call an endpoint to test instead of testing a particular service.
I think I am just caught up in the purity of the whole thing. Any guidance would be greatly appreciated10 Replies
my advice: don't use
IUnitOfWork at all, and don't mock the dbset or anything. instead, use testcontainers, and use WebApplicationFactory<> as intended, which is to say:
* use testcontainers to set up a brand new temporary database server container, matching your prod db engine; follow-up with applying migrations or otherwise building tables for your model, and seeding any data that needs to be seeded
* use WebApplicationFactory<> to load your program with test-specific customizations, like connection string to the test container, etc.; use this as an assembly-wide fixture to build it once to be used by all tests
* get an HttpClient from the factory and make an api call to whichever api you are testing for a particular test
benefits:
* don't have to mock something that's inherently unmockable
* better confidence that your code works the same as it does in prod (i.e. same db engine, same effect on orm, etc.)
* don't have to try to extract and set up specific services from your code for testing
* better confidence that your api calls work fully and completely, from top to bottom
* don't have to rewrite your test code in the future if you refactor internals; your tests don't rely on the internals, only the externally observable behavior from the api call
also: $whynotcaClean Architecture is not necessarily difficult to understand conceptually, but they're difficult to build and difficult to maintain. When you build a real project, you will quickly (as often evidenced on this server) run into questions like "Is this an infrastructure object or a domain object?" or "Should I implement this contract in the infrastructure or the application?". Questions that a) take your time to ponder and ultimately answer, and b) distract you from doing your actual work.
It also adds unnecessary abstractions, by forcing you to use layers: both unnecessary layers and unnecessary decoupling between layers. For example, CA would generally argue that you should abstract the database into repositories and services should depend on an interface for the repository. However, modern ORMs like EFC already implement the repository pattern, by abstracting the implementation of a query via LINQ. Furthermore, in most applications, there's only one implementation of
IXxxRepository - so why create the interface abstraction?
Instead, it's generally better to get rid of nearly all interfaces, keeping only ones that truly have more than one implementation; simplifying maintenance because any change to an api only needs to be done in one class instead of both class and interface. Smush all of the code down into a single Web project, or possibly two: Web and Services projects; removing any questions about "which project does this class go into?". Organize your code well in the Web project, with all of the User-related services/controllers/models/etc under /Features/Users/Xxx; all of the Order-related services/controllers/models/etc under /Features/Orders/Xxx; etc. Then it will be easy to find and maintain all of the code related to such and such feature. Any Infrastructure code (like emails, behaviors, startup code, etc.) can go under /Infrastructure/Xxx, and any non-infrastructure code that's not related to a feature can go under /Features/Shared.aand $cleanvsonion
What's the difference between Clean and Onion?
There's a lot of overlap between onion and clean. Clean is more TDD focused (I can mock anything and run a unit test on it), while Onion is more "infra-abstraction" focused (I can replace a database and the only thing I had to change was the implementation of service
IDatabase).
However, you don't need either of them.
* The problem with clean-level of TDD is that you start testing code and stop testing behavior, which means either a) that you don't have any confidence that the finished product works as expected, or b) you wrote a lot of unnecessary unit tests. It is far better to write one set of tests that works directly on a database and say "hey, my system has the behavior I expect".
* The problem with "infrastructure-abstraction" is that a) you don't really change infrastructure very often (how many projects have you worked on where you changed from oracle to mssql, for example?) and b) a lot of modern .net code already abstracts (namely, any modern ORM, like l2db or efc, already abstracts the specific database), and c) if you did change the infra, you'd want behavioral testing anyway, which goes back to integration tests instead of unit tests.have opinions on using repository and unit of work anyway
we are trying out more of VSA with this project, previously it was more n-tier but regardless I was leaning towards your first comment when it comes to using web application factory for most, if not all, the tests.
In regards to your comment about not using IUnitOfWork, do you inject the DbContext then? DbContext already implements the unit of work pattern, right?
do you inject the DbContext then? DbContext already implements the unit of work pattern, right?yup and yup
thanks for your quick replies. I am going to work on getting some of this setup over the next few days, so I appreciate it.
One more question, do you use Respawn to reset the database between tests?
absolutely not
* if you use test containers, then you get a fresh (and separate) db for each test run, so that makes it easier to write tests, not worrying about conflicting with other test runs
* however, tests should be idempotent in such a way that they do not depend or concern themselves with the results of any other tests. e.g. if you need to insert data, insert data, and whatever pk id value it get back is what it gets back; don't rely on it being a specific value.
this also improves reliability of your app because you're implicitly testing that api calls can run in parallel with other api calls.
thanks!