Unit testing Umbraco
Automated tests for software has become the de facto standard for professional development. For web and apps as well as traditional software. Using the best CMS out there is no excuse.
This session will show you how to write tests for the bits and pieces developers most often write for Umbraco.
- We'll start out by looking at unit testing in general, the different camps, and a bit about what to test and how to do it.
- There'll be a short introduction to code coupling and dependency inversion. Why it matters with Umbraco, and how mocking and stubbing can help us out.
- We'll look at the core's current status and how to lean on the core test library.
- We'll go through testing the most used base classes in Umbraco
- We'll have a look at also unit testing our backoffice UI code (JavaScript)
- When you walk out, you'll have no excuse for not automating your testing.
View transcript
VideoDays, Webinars8, WebinarSpots, RawMode, Thomas Madsen-Mygdal, Culture goin' home, убыкıt, выб resulted, Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. I'm sorry, I should have learned PowerPoint before I came here. Here we go. Good. So, surface controller has now become pretty straightforward. We usually need an Embraer helper. You should try to stick to it, because if you had gone to the current page, that might have taken a few more years. There are a number of stubbing steps to get running, but the Embraer helper already got that injected. So if you go through this .embraer.assignContentItem, then you have a stub published content item that you can do whatever you want to. It can return whatever that satisfies your test. For the surface controller, derive the base routing tests, get Embraer context and the routing context from getRoutingContext, and create stubbed content. There's also... I've got in a later study, but all the stuff that you can do on this .embraer code, so typed content from root, typed content by ID, all these methods are stubbable on an interface called itypedQuery something. I've got it in a later slide. So you can also stub all of these methods for your surface controller. So let's have a look at the renderMVC controller. In my sample, I want the renderMVC controller to do some logic with its state to find out which view to pass to the CTA form controller. So I'll fast forward to the repo again. Now I have this test called... Selecting CTA form. And, woe behold, there's a lot of code here. To test this controller that has this logic. It checks the user identity, whether it is authenticated, and sets the right CTA form on my content model. This could, of course, be a type model that you already get from Embraer, because this would already be typed, but I didn't do this for this project. But you can probably see that we want to test the logic in line 32 to line 35 here. And in my tests, I can get the stub for the HTTP context, and I can set up the user property on it, and I can say, return this generic principle, and my index method accesses HTTP context user identity, it will get this instance. And it will give me the right stuff. But to be able to instantiate my renderMVC controller, I have to go through all these steps. And I'll explain a few of them, but we'll see if we can't strip this code down to something more readable and usable. But anyway, if you had to test your renderMVC controller, you'd have to do a lot of things. We've already seen how to get the routing context. So now we have the routing context, and we've got the Umbraco context. I've also passed it an instance of route data for this test. And I forgot to say that the top statement here is even cooler than just getting the settings, the mock settings that Umbraco tests create for you. It also has this configure settings, which means that you pass all these mocked settings, to the singleton Umbraco config.4, which is the new way to access Umbraco config. You shouldn't use the other class. I forgot the name. But there is another older way to use settings. This is the new one, and it uses these stubbed settings, when you call configure settings. And then to avoid specifying the culture all the time when we use a renderMVC controller, we have to create this published content request, which is actually the best way to use the main object, deciding which content, it's the one that runs URL content finders, to find the right content for the current URL, and provide that to your renderMVC controller. And because it's a renderMVC controller, we also have to set up the view engine for MVC. And it has Razor view engines by default, which are going to go to your disk in the test. in the test assembly and try to find different views. So we have to clear it out, and we can pass it a stub view that just returns a view engine result. And you can also see up here that mock has this nice little syntax called itIsAny. And it basically means that when I call findView on the view engine, I can pass it whatever parameters I want. And in any case, we can create this view engine result with a stub view and the view engine. And so we clear it out to remove the Razor thing, and we add our own stub. And finally, we have to create a controller context that we can pass to our controller. And we have to set up some root data for MVC to do this. Let's just see if my tests pass. They do. By the way, if you don't recognize this test runner, it's ReSharper. I really recommend to run ReSharper if you're going to do tests and refactoring and everything. If you don't have ReSharper, run home and check it out. See what it is. It's great. In any case, I could also demonstrate that if I comment any of these lines out, my tests are going to break. We have to have all these lines of code. To be able to test a render MVC controller. So this way it's a bit more tricky. I have to inherit base web test, base routing test also probably works. Configure the settings, create a published content request, set up the view engines, and mock the root data. So my advice to you is, do not test render MVC controllers. Move your logic out to other more testable classes with less dependencies. And delegate to these classes from the render MVC controller. Let's have a look at that. Have that look. Fast forward to the repo again. And whoop! I've got two lines of setup code. Ain't that great? So what did I do? I created this new class called CTA form selection. And I inject the HTTP context base, which was what my logic actually relied on. I could even actually pass it to the user. The principle from the HTTP context. And down here in my content controller, I've now replaced the code with a new CTA form selection. And here it's all right. I've got to bind my code to this class, because it's in the same context. It's the same software. And I pass it the HTTP context. And I call select form with the model, and have it do its thing to my model. And its code is just a copy of the code we had in the render MVC controller earlier. But now in a test, all I have to stub is the HTTP context. And pass that to the CTA form selection class. And that contains the logic that I really wanted to test. I didn't want to test view engines. And my tests are the same. And I pass. And the code over here in the content controller, this code is something that you hardly can manage to master. If you manage to mess this up, and should have tested this, then you might be wanting to look at something else to do. I don't know. This is basic code. So you don't have to test everything. Test your actual logic with only the dependencies that you need. So let's look at how we can test Umbroco, whatever API controller. We've got a lot of those. And these are really great to do. To do kind of angular stuff and back office stuff. Also for front end stuff. And let's try to make something that would get the full name of the Umbroco user. So something from Umbroco's I user. And it will get, it first has to go and get some user data from the HTTP context, and then it has to access Umbroco's user service. I'm supposed to go like this, on the code. And I'll go back to the master branches here. This repo that I'm showing is on GitHub. So you can go and review all this code later. If you want. So let's see. Getting back office data. I'm getting sloppy with my testing, test naming here. But anyway, we're going to try to get some back office data via an authorized API controller. This should have been some behavior driven, nice business text. But anyway, you can see that the only thing I do, I derive this new class called base database factory test. And the name is a bit confusing, because we're not going to use a database here. But it's one of the uppermost base classes in the Umbroco tests library, which is really useful. And it can actually use a database, but we'll look at that later. And it has the basic getUmbroco context method. And I pass it true, to say that it should set a singleton, which means I can just insert it. And I can instantiate my simple authorized controller, which is an Umbroco authorized API controller. And I've got two methods here. One that just returns some data. And one that goes through Umbroco security context. And it will inside go and look at the HTTP context, get the security service and everything, and get the actual Umbroco user out from there. And we can see here that, just having the Umbroco context, creating the controller, we can actually already call the list method that doesn't use any Umbroco stuff. So an API controller can be really simple to test. But to have it access the services in Umbroco, we have to do something more. So I just set up a new claims principle on the HTTP context. And we've seen this before, but this time I need some user data. So this is Umbroco's back office identity, which contains all these properties. And the property that is interesting is the user ID. So Umbroco has to find the user ID to be able to look up the user in its service. And then I stub up a user, iuser from the Umbroco core, and say that its name should return this name up here. But now we're accessing, we're stubbing up something called a service context, and its user service, and telling it that it should return the user that I just created. And to be able to stub the services in Umbroco, you can now override with, I think it came in 7.5, but if you build a source it'll be there anyway. You can create the application context that is going to be used by the base classes. You can't set application context current, but you can pass it and Umbroco tests can do it. And we can use this method in Umbroco.tests called getMockedServiceContext, where the nice Umbroco guys have stubbed up all the services in Umbroco. And you can access these, like we do here, to stub up the results from these services. And all we have to do is create this database context, and you'll see what you have to mock. When you create something new for Umbroco, and you're not quite sure what it's going to be, but when you... Why doesn't it show? Well, you guys probably all know that you would get a preview of what you have to send in here. So when I do new database context, and it has all these dependencies, but they are all interfaces, you can just go ahead and say mock of i... i database factory, et cetera, et cetera. And it's going to work, because it's not really going to use them in this test. So just stub up anything that you see that is an interface. And finally, we can call the method on the controller, and assert that we got the right result. So, get the Umbroco context, override create application context, get the service context, stub that. Now, let's see how we can go fully classic and integrated. We've seen that all the tests for now has passed in less than a second. Nothing has taken any time. It takes a little while to build up the entire Umbroco context stuff and everything. But let's see if we can actually test against the database. Working with the database. And we still inherit the base database factory test. But we've added this attribute up here, which is an attribute from Umbroco called database test behavior. And it takes a few different parameters. But when you're going to use databases, use this one. The new DB file and schema per fixture. It's going to create a new Umbroco database for each of your test's fixtures. And then you can use that database in your tests. There's also one that actually creates new databases for each of your tests. Where you can imagine, if it takes like two, three, four seconds to create the database, you wouldn't want to do it for each of your tests. You lose the instance in the database. But the trick to avoid that is to call something on the database called begin transaction. Probably know what that is. And in the teardown method, you can call abort transaction. And then nothing happened on your database. So we've got this nice setup Umbroco database and you can do whatever you want to do it. But after your test, it didn't actually happen. And with that, we can actually create and save a data type in our tests. This is basic Umbroco bound stuff. I create a new data type definition. I use the data type service to save it. And the data type service that we earlier stubbed is now from the base database factory test set up to use the actual database in Umbroco. We can update it. We can create it. We can fetch the ID back. And this is actually an identity ID. So it has been created. We can update it. And we can refetch it. And we can assert that it was actually updated. We can even delete it. And check that it was deleted. So this is a good technique to use when you are writing persistence code. The stuff that will write to the database. You should probably go and create a classic test that does this. Or if you use several entities in Umbroco and what you want to test is really the persistence towards the database. Go and do this. But, as mentioned, it may severely impede your speed. So add the ignore attribute to your tests. You can still right click them and say run this test. But when you run your entire suite of tests those database tests will be ignored. So you can still prove for the most part that all of your code still works even though you didn't run your integration tests. Use the new DB file and schema per fixture. Otherwise just omit everything and you won't have a database. And use the transaction trick. You can go and get them and see how to do that. So finally a few tips. I mentioned earlier that with this.umbroco the umbroco helper that we stubbed it has another constructor where you can pass in I type published content query. And if you stub that you stub all the get content by whatever and you can just say return this list of I published contents. Which again can be new stubs. There might be quite a lot of stubbing code after a while. So shuffle that out into a test helper that has a general simple representation of the models that you use in your software. And reuse that across tests. There is also another method on the base database test called get XML content. Where you can actually return the result of the contents of an umbroco.com config file. So your XML cache on disk. You can return that from get XML content and the published content cache in your tests will now use that XML as content. But unless you stub up quite a lot of other stuff you can't use the properties but you can use the structure. So it's great for kind of URL provider tests or content finder tests and see if your logic does that right with your cached content structure. Override or create applications in context to be able to stub your services. And when you do set up and tear downs do override the base initialize methods because then you can control whether new stuff happens before the application context gets created or not. And you've got the same freeze resolution. Anyone who's been trying to test stuff that uses the resolvers all over umbroconos that can be really tricky. But you can call resolution.freeze and resolution.reset by yourself in this method so you can actually change how the resolvers work when you do these tests. We've seen a few of the base classes so these are just the most important ones. Use the base database factory test and the base routing test. And I didn't think we'd manage but we actually do have some time to look at the back office. How can we test the Angular stuff in the back office? Turns out it's actually quite easy. Angular is all about dependency injection. If you've done some Angular you've seen this long, hopefully not that long, list of stuff that gets injected to a controller. And JavaScript is dynamically typed so it's really, really easy to create stubs. You just create an object that has the method and it does nothing. And there's a stub for you. There's nothing checking that this was the actual class or interface. And if you use the library called jasmine it already comes with some spy stuff so you can also do real mocking. So let's have a look. I created this property editor that looks at the editor state which keeps the current content in its current property. So this will be the content that you're actually editing and it has a name. And I want my model to keep the name in a property called placeholder if it is anything. And this is the logic that I want to test. Whenever my property editor is shown it should go and fetch the name and show that as the placeholder for a text box and you can write in a text box then that will be the text property. Otherwise it will use and store the name. And so to test this I have to use this I can use this framework called jasmine. With ReSharper it's set up under tools JavaScript tests. You can enable jasmine support and you can set it to a version compatible with angular. This is built in but you have to go and download phantom JS to run it. ReSharper will tell you everything about it. And a jasmine test is a bit different. You describe your stuff instead. So you're a bit closer to behavior driven development here. And then I can go and stub out the entire Umbraco services module so I just create it without any dependencies without any controller. And then I specify that my module the module that I want to test I use this other construct in before each which is the same as setup. And when I've done that I can use something from angular called inject to get a hold of the controller factory and the root scope. And I can store that in scope variables and I can create a new scope from the root scope set it up with a model and as you can see I've just stubbed the service in Umbraco a bell called editor state with a current property that just returns blank object. And then I go ahead describing it. It initializes the value if it's nothing. I create the controller and I actually inject as we did earlier the scope and the editor state stub to my controller using the angular controller factory. And after it I can expect the value on the object to be this simple JavaScript object with a placeholder and a text property. And I can test that it sets the placeholder to the name of the content. So I go editor state current which is the content you're editing in Umbraco has a name and it's this. I create the controller and I have to call scope digest. That's what happens every time you touch the keyboard or click the mouse or whatever in angular. Angular will do scope digest on all the scopes to be able to check that this or this this or that happened or the values changed. And finally I can check that my controller actually set that value on my model value. And I can see that it also does it when I do further digests. And they all pass. And I run them with my .NET tests. That sounds great. Too bad I'm here. So yeah, there's a few things to keep in mind. If you're going to push your module as a dependency to Umbraco wrap it in a try-catch block otherwise it's going to blow up because you didn't actually stub Umbraco. Or you can stub Umbraco then you don't have to do that. Create the mock modules in before each same as setup. Stub as much as possible. And angular comes with this really cool stub called http mock which replaces the inner stuff in angular's http. So whenever you go http.get or http.post it will when you're using http mock check if you've set up an expected call to the server to some URL. And if it's the same URL it's going to return the same URL. This stubbed answer to you instead of going to the server. So angular is really friendly for testers. All the server stuff can just be stubbed out. And the documentation for all the Umbraco services are over at github. So you can see how they look, what should you stub, what should they return, etc. And finally you could always pop the debugger and just see what happens on the network. Copy the JSON, paste it in the network, paste in as your stubbed stuff and run with that server after that. Here's some resources for you. I really recommend that you go and have a look at the bottom one there called BDD. We didn't have time to look at that but that's really killer. You'll write your tests in plain English in business language and then afterwards you'll go and map up tests that will actually execute this piece of text. So do go and check that out. It's amazing. I'm going to be blogging about what you learned in this presentation on my blog which is blog.albeck.no The code you saw is on the top URL there. There's a few other articles. Questions? Yup, Dave. Do you call them a static? Well, yeah. You have to stub what is sent to the extension method. But the extension method is a static method so we can't really stub that. That would be really nice though to be able to stub interfaces. But just set up the dependencies that the static method will have and call that. Yeah? I'm going to come over. This script article is my article. Oh, thanks. I figured I could do most of the extension methods now. Yeah, right. So you'll probably be posting more on that. Is that the... Yeah, if you follow the script IO thing you'll see more about the extension methods. Yeah, that's great. Thanks. Other questions? Other questions? Must have been crystal clear. Excellent. Well, thanks for coming. I'll be around. I've got some BDD tests and everything. If anyone wants to see that just come and find me at the convention and we'll have a look. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Thank you. Greetings, 할� Latinx participants! Thank you. Thank you. Thank you.