CRITICAL for Plugin Architectures! - Configuration Tests in #CSharp
October 4, 2023
• 314 views
Plugin systems can add a lot of complexity when they grow in scale. This is the result of many modular pieces coming together to form one big application or service. Tests in C# and using xunit tests might be able to help us here!
In theory, we'd hope that by their design, plugins can keep being added and remain isolated - But who is responsible for making sure they all play nice together? Heck, who even knows about all the plugins we expect?! Let's see how we can add tests for plugin architect...
View Transcript
all right so you probably fall into one of these three categories the first is that you're not really familiar with plug-in architecture or plug-in Frameworks yet the second is that you've been watching my videos on plug-in architectures plug-in Frameworks autofac Blazer and how all of this can work together and you've started making your own applications with a plug-in architecture and the third category is that you're very aware of plug-in architectures but when you were looking at the pros and cons you said there's too much complexity that can arise when you're trying to put all these plugins into an application and figuring out how that's actually supposed to work in production now for the folks that are in category number one you can go watch some of my other videos first so I'll put a link up there and you come right back here and keep
watching and for the folks in categories 2 and three the reality is yes plug-in architectures do actually have this drawback where the complexity can grow a great deal especially if you're not really familiar with Plugin architectures or you don't have a lot of experience doing this there are things that you could be doing or might not be doing that aren't really setting you up for Success personally I think the pros outweigh the cons but the reality is yes complexity can leak in when you're using plug-in architectures and this arises because there are so many discrete things that have to be brought together to make the whole application work as a whole and sure yes you might not need all of the plugins to have the application work it could function with just a small subset of them but when you're shipping it to customers or
you have customers using a live service and you expect these plugins to be active this is where the complexity can arise in this video I'm going to walk through a project that I have and go through the different code organization and how I have different things broken out into plugins from there I'm going to explain how I've introduced some types of tests that I think really help me get an idea for how things are functioning but we're going to zoom out from there and see how we can apply this concept in general for different types of tests that might not fit in a traditional sense with that said let's jump over to visual studio and see what's up all right so I'm here in visual studio and I'm in my cerate solution cerate is the solution that I have for my service for nutrition that's
called meal coach. I've shown this in a couple of other videos but this is one of the services that I'm working on sort of on the side out side of work as a passion project now in meal coach or in the cerate solution that we have here I have this folder that's called microservices and it's worth calling out that it's called microservices but the way that my application actually works is that it is more like a modular monolith and by that I mean that I don't actually have separate microservices running I actually just deploy everything as a monolith however everything that we see in this microservices folder is a standalone independent type of service that could exist but I have it all integrated into one and if that sounds a little bit confusing I'm just going to throw out that they're all plugins and because
they're all plugins I can dynamically load them at runtime to get the functionality that I expect to have in my application and without getting into the details this really just means that my application that I end up launching is really just a shell application it doesn't do very much at all but what it does do is load these plugins which actually are responsible for the bulk of the logic that the service has and that means if I really just wanted to have four example a hello service well I could actually just stop packaging all my other plugins and just have a Hello plugin and my program would actually launch and I'd have a service that really just says hello but because my expectation is that my meal coach service actually has all of these pieces working together and I don't want to deploy them separately
I do have all of these plugins bundled and they get deployed together but because I have all of these plugins in this folder when I do want to go run my service I want to make sure that everything's actually configured the way that I expect and if I go back to the example I just just made I don't just want to have a Hello service I want to make sure I have a Hello service a job service a meal plan service so on and so forth my expectation is that all of these services are available and that must mean that I should be able to check the plugins that I have to make sure that I have access to everything that I expect here okay so I want to pause for a second because if you start to conceptualize this this might seem a little
bit backwards from all of the benefits that plugins really give us plugins in theory should allow us to extend an application without the core application having to know about anything to do with the plugins and I still think that's true that's really the goal here when I go to add a new plug-in in my case it's a new service that I want to extend it's part of my main offering I don't want to have to go touch the core application or some other service any other part of the code to make it work I just want to add the new plugin so when I'm talking about checking a configuration for my core application it sounds a little a little bit backwards that that configuration test and my checking to make sure I have all the plugins I need it sounds backwards that it would be
able to know about that because shouldn't we just be able to add plugins dynamically and this is just one of those times where it comes down to use case and being pragmatic in my situation I don't have plugins that are created from a third party and I don't have plugins that I need to actually load dynamically as the service is running really I have plugins just to give me as the developer the flexibility to land features without having to disrupt anything else I also leverage plugins because in the future if I do want to move to microservices or have some other way that I package these things together maybe some of these Services don't always make sense to be grouped and I do need to kind of modularize them and separate them I have that ability built right in and I think that's an important
call out I'm not using plugins to extend it to third parties I don't need to worry about that it's really just a convenience thing for my development so what are the implications here if the core application or some part of the core application must know about all of the plugins well let me clarify because it's not actually going to be the core application that needs to know it's the test for the core application and more specifically the integration tests I have when I go to test that core application in a lot of my content when I talk about testing fundamentally it always boils down to having confidence in the changes that you're making and for me when I all of these tests for all my different plugins and they're addressing how that plugin Works individually when it comes time to actually testing the core application
really for me that comes down to making sure that the core application has all the pieces that it needs and if we go back to what I said earlier my expectation for the core application is that it has all of these pieces that I've introduced so the way that I've approached this is essentially that I have a set of tests or a style of tests that almost act like a manifest for what I expect to have in the core application and that means if I have an expectation of the different plugins that should be there then I can run different tests and different checks to make sure they're behaving as I'd expect this does seem like it goes a little bit in the face of some of the other plug-in things that I mentioned like being fully extensible because yes if I go to add
another plug-in and I want to go extend the core application I don't have to go touch the core app but I have to go touch the functional test if I want to make sure that that core app application is tested accordingly now that means depending on how I've set up my test for the core application I could have it such that it remains brittle and if I add a new plugin that's not expected those tests would fail the other way to do it is that if I add a new plugin none of the test fail it's just that I don't have that extra coverage that I would expect to check for that plugin now what's right and what's wrong well that's totally up to you in your use case but for me the way that I've approached it is that I'm not protecting against extra
plugins I'm protecting against making sure that plugins don't actually break what's already there and that includes the set of tests so if I were to add a new service I don't want my integration test to break but it will mean that I have to go back to these tests to make sure that I have coverage over this new plugin now that's probably enough rambling and talking with my hand so let's get back to visual studio and I'll show you what I mean by one of these tests okay so back in Visual Studio I'm going to go to configuration test and you'll see that this file is actually inside of a project called cerate Web API test functional and cerate web API so this project right here is actually the entry point to my service so this is really just the functional test project for that
service now this test file actually only has one test but if I go to explain it the pattern should be applicable for other types of tests that I'd want to create to give me the coverage that I want on my plugins so before before I jump into the test case itself I just want to talk about some of the setup that I have here because this is a pattern that I've shown in other videos and your situation might look different than this in fact the code I can guarantee will look different but the actual process and thought behind what I have here might be very similar so what I'm doing in this case is making sure that I can stand up an entire application so that I can resolve all of the dependencies properly and I need to do that because I'm going to be
checking for plug-in information if I were to use things like and kind of take shortcuts just to be able to get instances to things they're not going to represent the real things that I need in my application so what this code does is actually inject some of the pieces that I need like a SQL database and a mongodb database so these will actually be containers that go run where the tests are being run and then this code here in the Constructor is just a builder pattern that I've created that actually allows me to set up an API client to talk to an act actual server instance that's running and again I'm not going to go into all the details on how and why this works but what I'm able to do at the end of this is basically resolve dependencies out of a dependency container
that is based on a real running application okay now to look at the test now this test here looks like an absolute mess and if you're going holy crap you're right this can definitely be improved but let me explain what's going on the purpose of this test is that I actually have different plugins that I need to look at which is why this list is so long and what this test does is ensure that I have caching enabled in all of the places that I expect it now fundamentally to go check if I have caching enabled this could be done in a ton of different ways depending on how I have things configured but for me the most effective way because I'm using autofac is I want to make sure that the actual references for the types that I'm resolving are exactly as I'd expect
so if we look at the different types that I have as part of this inline data list here we can see that I have things like a user with metadata repository a user repository user off repository and a whole bunch of different repositories and the reason I have these here is because each of these repositories actually has caching that I want to ensure is enabled now in order for this to work yes it does mean that my test project actually has to reference and know about the different plugins so that's a little bit messy totally agreed doesn't have to work this way but what I could also do is leave it Dynamic so that I don't really need the references to the specific types but instead I have some other way to go resolve the types and maybe just use some manifest that can look
things up by name these things would still be coupled together in the sense that I need to have knowledge about the different plugins that I want to test however I wouldn't have a hard dependency on the actual binaries to be able to do this type of referencing here now the reason this works okay for me is because I'm taking a shortcut with my plugins I actually do reference all of my plug-in projects inside of Visual Studio inside of this cerate web API project and I do that because like I said earlier I'm not actually trying to deploy these things as microservices I'm just trying to get the advantages of having all of these pieces separated so by referencing the projects I actually get the convenience of being able to press the play button in visual studio and have all of the binaries copied and generally
everything works as I would expect in your environment say you have a bigger team or you have multiple teams in your organization working in an application that has a plug-in architecture this totally might not work for you in fact you might have different build systems building each of these pieces and then those pieces are getting dropped into a different directory at build time or deployment time whatever but in my particular case case I have this shortcut where I do have all these binaries accessible to me now the point that I'm trying to make with a test like this is that I actually have patterns that seem to go a little bit against all of the other things I was talking about with plugins and you're probably wondering well why like why would I do something that seems to go against the pattern but it's to
demonstrate to you that when I'm trying to build confidence in the things that I'm creating I'm trying to be pragmatic about what I'm doing and looking for things that seem to work I've already mentioned to you as I've explained this that there are opportunities that I could refactor this out go build some type of manifest where I can have text Bas lookups and check all the types so on so forth but that's just extra work at this point it will literally offer no value to me if I go do that when I already have it working like this if there's a point in the future where I need to break these dependencies and I can't actually have this project referencing these other plug-in projects projects then certainly I can go look at refactoring this but what this test is affording me to do and this
style of test would afford me to do is actually ensure that my main application is configured to use plugins the way I would expect so this test in particular like I was mentioning is just looking at caching being enabled where I would expect but I could also just go write a test that basically iterates over all of the plugins that I expect to have available so if I go back to the left here in the solution Explorer I could go double check to make sure I have a plugin for each one of these folders that could easily be another configuration test then I could go run and would absolutely offer value to explain why that offers value one of the things that I added most recently was actually this hello service if you recall I mentioned that the project cerate web API must reference all
of the services that it intends to have integrated into it if I forgot to include that reference I'd have compiling code here for this hello service but it wouldn't actually end up in the bin directory ever if I want to ensure that that's done properly I could absolutely go write that test that I was explaining and make sure that I can resolve a type or some other check to ensure that I have access to that service being resolved when I would expect and there's lots of other things you can consider here so for example if you needed to have your plugins load in a particular order if you needed to make sure that in certain situations like debug versus release that things were excluded or included you could absolutely write test that check for that but again I'm using patterns like this that seem to
go against plugins because they're giving me the confidence I need that my core application is configured how I would expect so bringing the conversation back to complexity and for folks that might not really be into plug-in architectures just because there is so much complexity I wanted to Showcase that you can actually take a different approach with your testing that might give you some of that confidence back now the cases where there is going to be a lot of complexity probably don't align to the pattern that I just showed you because you'll probably have different teams that are delivering plugins and some build system or continuous integration system is going to ensure that those are dropped in the right spot it might look very different at different places of course but when that kind of thing happens what kind of test do you want to have
in place to make sure that things are operating as you'd expect the way that I had the test set up in Visual Studio that I just showed you may not work because you'd have to have references to those DLS if you're dropping them in dynamically and you don't have a reference ahead of time that's probably not going to be the solution for you however you might be able to implement something similar to that and actually have a harness that goes and resolves the plugins that you'd expect back to that manifest idea I was talking about maybe where you actually have a list of the plugins in text form or the path to the plug-in ensuring that those are loaded and you could do checks to make sure that they do resolve properly and realistically this might be a test that you can't go run in
Visual Studio depending on what your organization looks like how your project or your service is set up this might be something that actually requires a build system to put these pieces together before you can go run a test like this successfully to have it run in your local environment in Visual Studio it would mean that you need to be able to pull down these other pieces so say other teams U plugins their services whatever they would have to be dropped into something on your computer I personally don't have to deal with that in my current case so that's why my approach to this works the way it does but if you want to apply a similar technique the point that I'm trying to get at is that you might need to consider that this is a type of test some type of integration functional test
that runs in your continuous integration system and not actually something that you just go Press Play in visual studio so to summarize this yes plug-in architectures can absolutely add complexity into the services and the projects that you're creating and one of the ways that we can try to have confidence with all of this complexity is by writing tests but the style of test that we might want to write to give us that confidence back might not feel like the right thing to be doing because it seems to kind of go against some of these patterns that we've been exploring with plugins the idea that plugins can just be dropped in and things work and you don't have to touch any you know Central code or configur anywhere that seems to be something with plugin architectures that we really want to have but when it comes
to testing the configuration of all these plugins working together the reality is that that centralization that that idea that something some Central thing needs to know about the configuration that seems to shine through and if you can kind of work past that mentally then you can start to think about Solutions where you say well if I have something Central that seems to know all these plugins are needed then I can start making solutions for that and one of the ways that you can try to work past that is consider the fact that if you or someone else knows all of the plugins that are needed that's already a central thing that knows about the configuration why can't you codify that why can't you put that into a config that can be consumed so this was mostly just a thought exercise to get you thinking about
how you can actually work through the complexities of plugin architectures by writing tests that look at your configuration so if you've been build building out an application with plugins if you're reaching the point where you're going hm I wonder how I'm going to test if this all works this might be the next perfect step for you and if you're someone that was saying hey I don't like plug-in architectures way too complex I'm hoping that this gave you some insight into how you can look at writing tests to actually check that that complexity is in order so thanks so much for watching I hope you enjoyed and we'll see you next time
Frequently Asked Questions
What are the main benefits of using a plug-in architecture in my application?
I believe the pros of using a plug-in architecture outweigh the cons. It allows for greater flexibility in adding features without disrupting the core application. You can dynamically load plugins at runtime, which means you can extend the functionality of your application easily.
How can I manage the complexity that comes with plug-in architectures?
Managing complexity involves writing configuration tests that ensure all necessary plugins are loaded and functioning correctly. These tests help me verify that my core application has all the pieces it needs, which gives me confidence in the overall system.
What should I do if I want to add a new plugin to my application?
When adding a new plugin, you'll need to update your functional tests to ensure that the core application recognizes the new plugin. While you don't need to modify the core application itself, it's important to maintain your tests to ensure everything works together as expected.
These FAQs were generated by AI from the video transcript.