Cost Comparison: Compiler vs Code Reviews vs Architecture Testing
54sHighlights a surprising cost-benefit analysis that challenges common practices, sparking curiosity and debate.
▶ Play Clip"Delivers exactly what the title promises — a practical guide to replacing manual reviews with automated architecture testing."
The video discusses three approaches to enforcing software architecture: using the compiler, code reviews, and architecture testing. It compares their cost and performance, then demonstrates how to implement architecture tests in .NET using the ArchUnit.NET library. The focus is on practical examples like layer dependency checks, naming conventions, visibility, and co-location tests.
The compiler is the most affordable and fastest approach, code reviews are the most expensive and slowest, and architecture testing sits in the middle.
Architecture testing is an automated testing approach where you define your architecture rules as test cases.
ArchUnit.NET is recommended over NetArchTest.Rules because it has advanced features and is better maintained.
You can write tests to ensure the domain layer does not depend on the application layer using the types function and check method.
Naming conventions can be enforced, e.g., command handlers should end with 'CommandHandler'. Generic types can cause failures, but you can use 'contains' or exclude namespaces.
You can enforce that certain types are internal or public, and tests will fail if the visibility is changed.
Co-location tests check that all files for a single use case are in the same namespace, ensuring related code stays together.
The compiler is the best-case scenario, but it's not always possible. Roslyn analyzers can help but require additional code, making costs similar to architecture tests.
What are the three approaches to enforcing software architecture mentioned in the video?
The three approaches are the compiler, code reviews, and architecture testing.
00:27
Which approach is the most affordable for enforcing architecture?
The compiler is the most affordable and fastest approach.
01:10
Which approach is the most expensive for enforcing architecture?
Code reviews are the most expensive and slowest approach.
01:25
What is architecture testing?
Architecture testing is an automated testing approach where you define your architecture rules as test cases.
01:54
What are the two popular libraries for architecture testing in .NET?
ArchUnit.NET and NetArchTest.Rules.
03:12
Why does the author recommend ArchUnit.NET over NetArchTest.Rules?
ArchUnit.NET is recommended because it contains advanced features that the competing library does not.
03:38
How do you write a test that the domain layer should not depend on the application layer?
You use the 'types' function and define rules like 'should not depend on any'.
04:45
What does the 'check' method require in ArchUnit.NET?
The 'check' method requires an 'architecture' object, which represents all available types for the checks.
06:07
How can you fix a naming convention test that fails due to generic types?
You can use 'should have name ending with' or 'contains', or exclude the namespace from the test.
09:21
What do co-location tests check?
Co-location tests check that all files related to a single command handler or query handler are within the same namespace.
11:35
Architecture testing as a middle ground
It highlights a balanced approach between cost and performance, offering a practical alternative to manual reviews.
01:54Writing layer dependency tests
Demonstrates a concrete, executable way to enforce clean architecture rules using ArchUnit.NET.
04:45Enforcing naming conventions
Shows how to automate a common review point, saving time and ensuring consistency.
07:56Co-location tests for use cases
Introduces an advanced pattern that ensures related code stays together, improving maintainability.
11:35Compiler as the ideal but limited enforcer
Clarifies that while the compiler is best, it can't catch everything, justifying the need for architecture tests.
13:39[00:01] know what is the cost of maintaining the architecture that you so carefully implemented on your project? In this video, I'm going to discuss a couple of approaches that can help you enforce your software architecture, ranging from
[00:14] the most affordable one to the most expensive one, and how you can go about implementing them. Let's first talk about the couple of approaches to enforcing software architecture. And I'm going to give you three approaches. The
[00:27] first one being the compiler, the second one using code reviews, and the third one using my favorite, which is architecture testing. So, how do these three approaches stack up in terms of their cost? So, let's have a column for
[00:44] that. And then another thing I'd like to compare is performance. So, we're concerned with cost and performance of these three approaches to enforcing software architecture. Let me see how I can outline them so that we can see
[00:56] everything nicely on the screen. I think this should do it. So, let's start from the cost. Here at the top, we have the most expensive. And here at the bottom, we've got our least expensive approach, which we're going to denote with zero.
[01:10] So, when it comes to the cost, the most affordable one is going to be using the compiler, because this is fast to execute. And if you can enforce your compiler, then this is going to be the most affordable version. The most
[01:25] expensive one is going to be using code reviews, because this requires someone who understand the costs of enforcing the software architecture, and also having to spend time to properly construct a review that can help someone
[01:39] understand what they are doing wrong. So, then we've got our last approach, which is somewhere in the middle. It's not exactly the most expensive one, and one. And I'm going to place it like this, because architecture testing is an
[01:54] automated testing approach where you define your architecture rules as test cases. So, it's not a free option because you still have to spend some time writing these test cases. I really to see how this stacks up in terms of
[02:08] performance. In here, we're going to arrange this from slow to fast. So, when it comes to the slowest approach, again, we're going to have code reviews at the top because they simply take a lot of time. But, someone has to evaluate some
[02:22] pull request and make sure that nothing has been broken. The fastest and most ideal approach is again using the compiler. And as you might imagine, architecture testing is somewhere near the compiler because architecture tests
[02:34] going to be significantly better than using code reviews. Now, don't get the feeling that this makes code reviews obsolete. That's not the case. I just want to paint a compelling picture of why you should care about architecture
[02:47] testing and how it compares to some of the alternatives. Now, let me show you how we can introduce a couple of architecture tests into our code base. What I have here is an application that's implemented using the clean
[02:59] architecture approach. However, what we discuss in regards to architecture testing applies to any type of architecture that you might think of. The number of rules that you might want to enforce may vary, but the ideas are
[03:12] to use for architecture testing? There are two popular libraries that you going to show you the one that I think called ArchUnit. NET. There's also a
[03:26] library called NetArchTest.Rules. However, that one has fallen in popularity, and I'm also not using it as much because it's not as maintained, even though it's feature complete. And I found that the ArchUnit.NET library
[03:38] contains some advanced features that the competing library does not, which is why I recommend using this one. And I'm going to show you how we can implement it inside of our test project. So, let's look for some NuGet packages. And we're
[03:50] going to browse for ArchUnit.NET. Now, depending on which testing framework you're using, you're going to install the respective library. I will be using the xUnit variant, so let me go ahead and install that one. Here's also the
[04:02] other popular library called network test.rules, and I have talked about this Now, how do we start with our architecture testing? It all revolves around using your namespaces and your assemblies to write some automated
[04:18] checks to ensure that you are respecting whatever rule you are trying to enforce. For example, in clean architecture, it makes sense to test whether we are respecting the correct direction of dependencies between our components or
[04:32] layers inside of the application. So, let's call this the layer tests, and we're going to start here. And I'll make sure this implements my base test class. So, let's say we want to start with a sample rule that the domain layer should
[04:45] not depend on the application layer. So, let's make a test called domain layer should not depend on and then application layer. And how we would write this is using the types function, which is available on the arc rule
[05:00] definition static class, which I'm going to import using a static import. So, then I can say types that are, and this is how we define the types that we want to test. Now, we need a way to provide our types. And this library offers a
[05:13] couple of ways to do this. I'm going to show you one example, which uses an I object provider. And we're going to provide a set of types, which are going to represent our domain layer. And how do we define these types? Well, we can
[05:26] say types, and you'll see that this is a common entry point into defining our tests. And we're going to say that reside in assembly, and we've already got our domain assembly in the base test. And we can even give our types a
[05:40] descriptive name, so let's call this the domain layer. So, back in our test, we want to say types that are in the domain layer, and then we can define our rules. I'll say should not depend on any, and then again, we need to provide what we
[05:54] can't depend on. So, this will be in the current example, our application layer. So, let me rename a couple of variables, and we have to pull this from the application assembly. So, now I can say that the types in the domain layer
[06:07] should not depend on any type in the application layer, and then finally, we can call the check helper function. And this requires what's called an architecture in the ArchUnitNET ecosystem. So, this just represents what
[06:20] are all of the available types that you want to use for your checks, and I want to define this in my base test class as an architecture. Let's give it the same name, and you create this by instantiating a new architecture loader.
[06:34] So, let's call this the arc loader. And we're going to load our assemblies, which we conveniently already have here. So, I'm going to pass them in one by one, and then we're going to build our architecture. So, now we can use it
[06:46] inside of our test to pass in to the check method, and let's go ahead and run our test to see if we are respecting this rule. So, I'm going to run our see, this will complete pretty quickly, and you will see that our test is
[07:01] passing. So, this is how we can enforce a simple rule that the domain layer layer. Let me show you a couple more examples, which I'm going to drop in in order to save us some time. And what I've done is added more tests. It
[07:15] dependencies are correct. So, we want to make sure that the domain layer doesn't presentation layers, and this is expected because it sits at the core of the application layer, which should not depend on infrastructure or
[07:30] presentation. And lastly, we've got a check that infrastructure doesn't depend on presentation. So, let's run all six of our tests, and you can see that all of them are passing. So, in the current implementation, we are respecting the
[07:44] rules that we defined inside of our architecture tests. What are some more interesting examples that we can enforce using architecture tests? Well, one interesting thing could be naming conventions if that is something that
[07:56] what that could look like. We're again going to implement our base test class and let's define our test case. And here I want to enforce a rule saying that command handlers, because I'm using explicit commands and queries, and I'm
[08:10] going to say should have name ending with and let's say command handler. So, how could we write a test like this one? Again, we start with the types helper static using statement so that I can write my test like this. And instead of
[08:26] types, let's be more specific and say classes and we're going to say that implement interfaces and let's say our first interface should be an ICommand handler or we also want to make sure that we check the ICommand handler that
[08:41] returns a response. So, this should encompass both our implementations. And then I want to say should have name ending with and we're going to say command handler. So, then let's check this by passing in our architectural
[08:55] object and I'm going to just run this test to show you what the results are pass, but you can see it's actually failing. And the reason for that is given here. Now, if I look into what's actually causing my test to fail, it's
[09:08] my cross-cutting concerns implementation using the decorator pattern. I've got some classes called command base handler and command handler for the logging and validation decorators, but the bigger problem here is that these are actually
[09:21] generic types, which means C# is going to give them some custom names at runtime. You can see the one and two here, which is what's actually causing ways around this. Instead of checking that the name ends with, you can do a
[09:34] contains or you've also got an option to exclude this namespace from your architecture tests. Now, you can do so before writing your assertion. And for before writing your assertion. And for example, I can say do not reside in
[09:47] namespace, and I'll pass in my namespace as a string. So, let's drop this in here. And now, if I rerun my test, it's going to check all of my command handler implementations minus the decorators. And this is going to cause my test to
[10:01] pass. A couple more examples could be enforcing the naming convention for our query handlers and our validators. We want to make sure that the names end with query handler or validator. If you've got some other naming convention,
[10:13] you can of course implement that. And you can see our tests are passing. visibility of our types if that is something that you want to enforce. For example, I want all of my command handler and query handler
[10:26] implementations to be internal. And I can do so by selecting these classes and internal. You can also enforce things like if you want some types to be a record or you want them to be explicitly public. But in my example, I want them
[10:41] to be internal. So, let me run those tests under the visibility tests section. And you can see that my tests are going to pass. However, if I go into some command handler implementation and change the visibility to be public and
[10:56] then rerun these tests, we're going to see that we will get a failure scenario. So, in this case, it's a command handler and the test lets you know which command handler it is so you can go and fix it. Conversely, I can go ahead and make it
[11:09] internal, but just slightly alter the name. And this will cause my naming convention tests to fail while my visibility tests are going to be passing, which you can see is the case here. So, let's revert that. And I want
[11:21] think is a pretty interesting use case for architecture testing. And I'm calling this co-location tests. What this type of test does is it checks that all of the files related to a single
[11:35] command handler or a query handler are within the same namespace. Now, in my case, I have multiple files for a single use case. They are going to be in the same namespace, and you may have to slightly alter this if you are using
[11:47] nested types to represent your use cases. So, how this works is it selects the types from my application assembly that implement the respective handler interfaces. And then what we're going to do is to construct a theory data object,
[12:01] which is a way to pass in strongly typed parameters into your xUnit tests. And here we're going to add pairs representing our handler types, our specific command or query types. And in the test, we just want to make sure that
[12:13] they are in the same namespace. So, right now, if I go ahead and execute all of my test cases, including the newly added colocation tests, you're going to see that all of them are going to pass because I'm respecting this
[12:27] codebase. But if I go into the application layer, and let's say I take the complete to-do command and we move it into the root namespace for whatever it into the root namespace for whatever reason, let's make sure we save this.
[12:41] And then I'm going to rerun my colocation tests, you'll see that that one test case will fail. Now, I may have to recompile this and run it again object, but we're going to get a failure case here. And it's telling us which
[12:55] type is failed. So, now if I go back and take my complete to-do command and move it back to where it belongs, make sure I save this, I will have to rebuild this test case. But nonetheless, when we rerun this now, you will see that our
[13:10] tests are again passing. So, colocation tests, this is something I haven't might find it interesting. We can extend this example to also affect our validators, any request or response objects. There's really no limit to what
[13:24] you can do here. And again, a reminder of the cost versus performance analysis between using a compiler, code reviews, and architecture testing for enforcing world, using the compiler is the best-case scenario. However, it's not
[13:39] always possible as the C# compiler isn't almighty. Now, you can implement Roslyn analyzers, but these will also require you to write additional code, which is going to increase the cost to be somewhat similar to architecture tests
[13:52] as this is still code you have to write and maintain. If you want to grab the source code with all of the examples, you can do so from the pinned comment. It's going to be right below this video. If you enjoyed this discussion about
[14:04] architecture testing, then leave a like on this video so more .NET developers can find out about it. Thanks a lot for watching and until next time, stay watching and until next time, stay awesome.
⚡ Saved you 0h 14m reading this? Transcribe any YouTube video for free — no signup needed.