DEV Community

Cover image for Pushing testing inward
Chloe
Chloe

Posted on

Pushing testing inward

Recently I've been working on rewriting Dandelion to use Clean Architecture.

Right now, the architecture for the parts I've rewritten looks something like this:

Architecture Diagram

There are a lot of arrows, and I'd like to simplify that, but the important part is that they all point inwards. Lower-level (farther out) components depend on higher-level (farther in) components. This is the Dependency Rule.

So far, during my rewrite, I've been testing things as I go using the GraphiQL UI. This has been helpful for making me confident things are working end-to-end, including GraphQL, MongoDB, Express, and Passport.

However, starting yesterday I'm trying to test using Jest tests instead. Right now I'm testing at the "Interface Adapters" area, meaning I'm testing everything inward of this circle. My tests are testing Enterprise Policy (Entities), Application rules (Interactors), and Interface Adapters (Controllers and Presenters.)

Doing this in addition to (if not instead of) GraphiQL testing has a couple advantages:

I'm forced to see where my Mongodb plugin has unneeded complexity

For the tests, I'm writing an in-memory "database" plugin and using that instead of the Mongodb plugin. To get the tests passing, the plugin needs to be able to achieve the same things the Mongodb plugin achieves.

They're both using an abstract plugin interface, which is a great start for separating database/persistence/storage code from enterprise/application code. However, as I write the new in-memory plugin, I've noticed that the Mongodb plugin does some things that aren't strictly necessary for persistence or performance, such as privacy checks.

There also are a lot of type-switching factories in my code. There are several different types of history events that can exist on an aid request -- it was created, someone commented, someone started working on it, the recipient name was changed, etc. Since history events can be represented in multiple different contexts, e.g., pre-commit, post-commit, application code, DB-specific code, there are multiple places where all of these different types of history events are implemented as several different classes implementing the same interface.

Maybe I will find a better solution to this eventually. However, the takeaway for this post is that the Mongodb plugin has a couple of different type-switch factories to convert subclasses of one interface into subclasses of another interface, and now I'm having to re-implement that in my in-memory DB plugin. Going through the process of writing a test plugin is making me see where there's an opportunity for code simplification.

I'm forced to revisit the interface between GraphQL and the controllers.

The test code I'm writing is similar to the GraphQL resolver code, in that it calls controllers and then examines the values in the presenters that are returned.

When writing tests, I like to make the interfaces between the test code and the application code as simple and clear as possible. I also like to keep logic out of the test code where possible.

I'm currently having to implement a lot of logic in test code and sometimes bypass the controller interfaces. This is showing me opportunities for improving these interfaces.

There's a clear path to improving the interfaces of the controllers: Do whatever makes the tests simpler.

It's easier to think about what use cases I want to support

There are a lot of new features I want to support in the app. However, it can be overwhelming to think about supporting them through all the parts of the codebase, including entities, interactors, controllers, Mongodb, GraphQL, UI design, and UI implementation.

Now, thinking of the tests as another "client" of the application, I don't have to support all those parts. I just have to support entities, interactors, and controllers. And mock plugins for the framework/DB adapters.

With this simplified scope, it's a lot easier to think through the interfaces and requirements for the next few things I want to do, such as:

  1. Allow people to create their own accounts
  2. Allow people to create their own sharing groups
  3. Allow people to add other people to their sharing groups

And, as a final bonus (this is one of the main purposes of Clean Architecture), after implementing these new use cases with the tests as the client, I can be confident these inner rings of the architecture will still continue to work well even if I switch to a different database, API framework, or execution environment.

As I progress, I'd like to think about moving my tests even further inwards -- unit testing some of the modules within the Entities directly, and then move out to testing the Use Cases directly, without going through the controllers. This might allow me to develop new use cases and/or controllers more easily and confidently, since the interfaces of these inner layers will be cleaner, more complete, and better tested. However, it's also possible the level of overhead (in terms of code maintenance) required for this much testing will be excessive. We'll find out as we go!

Top comments (0)