 Today we'll talk about tidying up your tests and using component harnesses to write tests that bring you joy. I'm Alisa Duncan. I'm a senior developer advocate at Okta, an Angular GDE, co-organizer of Angular KC Meetup, and on the core team of Engie Girls. I love learning about, discussing, and writing automated tests. I also enjoy reading books and drinking wine. You can follow me on Twitter, GitHub, and Dev2 at Alisa Duncan. First, let's talk about tests that don't bring us joy. Do your component tests feel cluttered? Is there a lot going on in your component unit tests? It's hard to follow. Is it difficult to read and understand your component unit tests at a glance? Or maybe your teammates have said it's difficult to make changes or add new features into your component unit tests? Or maybe nobody wants to work on the unit test because they're so difficult to do? Do you want tests to focus on testing behavior without having to deal with querying the DOM or worrying about implementation details? You can tie to your tests and focus on writing meaningful tests with test harnesses. Test harnesses are a set of APIs in the Angular CDK that support testing interactions with components. The idea for this is based off of the page object pattern, which is commonly used to write integration-style tests. All Angular material components have test harnesses for them using the Angular CDK testing API as a base. And that is fully available for all components starting in Angular material v12, although they ever started in v9. So if you're not on the latest versions of Angular, you might still have access to test harnesses. This is useful because it allows you to write tests that are easier to read. You're able to tell to glance what's going on with the tests and be able to contribute to it much easier as well. And your tests will use an API to interact with the component, which makes your tests more resilient and less prone to breakage if anything in that underlying component changes. And that allows you and your team to write tests that focus on behavior instead of worrying about the implementation details of how to write the test. If you've written component-style unit tests where you have to query the DOM a lot, you know what I'm talking about here. So let's write a test and compare and contrast a test with and without harnesses so we can see the value. To do so, we'll have a sample application. Here we have a tidying task to do app. Look up all the bells and whistles about this app right now and skip to the part that all to do apps have, which is the list of tasks. In this to do app, when you complete a task, you can check it off. And when you check the task, we'll add a strike through through the text showing that it's completed. This behavior of checking the checkbox and adding the strike through on the text is what we'll look at in our example. So for context, the code that we'll look at looks like this. This is the component and it is using Angular material. If you haven't used Angular material before, the thing to note is that all components have a map prefix. So for a checkbox, we have a map checkbox. And in our case, we're actually just looking at the ng class attribute directive where we're adding the CSS class tidy tasks completed if the task is completed. In our example test without harnesses, this is what one might look like. I'm skipping all the testbeds set up in this and diving straight into the test. So in this test, what we're trying to do is make sure that that CSS class tidy tasks completed matches up with whether the checkbox is checked and the order of operations might look something like this. The first thing we want to do is get the checkbox. And we can do that by querying the DOM and looking for the selector map checkbox, which is the Angular material checkbox element. Now we want to verify that the checkbox starts off unchecked. To do so within that mat checkbox element, we query for the input element and then look for the checked attribute on the native element and make sure that that starts off unchecked. And we also want to verify that the CSS class tidy tasks completed isn't on the checkbox. So we go back to the original checkbox element, look at the native elements class list and verify that the CSS class tidy tasks completed isn't on there. Now we want to check the checkbox. To do so, we query the DOM again to find the clickable element of material checkbox and that's the selector mat checkbox in our container. Notice we'd only know this by looking at the mat checkbox code or if we dug around in Chrome DevTools. So we query for mat checkbox in our container and click and run change detection. Now we can go back to the input element and verify that the checkbox is checked. And we go back to the original mat checkbox element. We can verify that the tidy tasks completed CSS class is in the class list. So that was a lot. We had to query the DOM a lot. We had to understand the internal workings of mat checkbox. And it's not clear at a glance what exactly we're doing here. Make sure we have a description on the test, but it doesn't seem to match with what we actually had to do. In contrast, a test with test harnesses might look something like this. Notice it's a lot shorter. In order to make it easy to compare and contrast, we're going to follow the same order of operations in the same sequence. So the first thing we want to do is get the mat checkbox test harness and we're using a loader to get that. We'll talk about that API here in a bit. And then we want to verify that the checkbox starts off unchecked. We can do that using a public API on the checkbox harness, which makes it really easy to tell what we're doing here. Next, we want to verify that the CSS class tidy tasks completed isn't on the checkbox. Now we want to toggle the checkbox to check it. And we can do that with a public API as well. And now we can assert that the checkbox is indeed checked and that the checkbox has the class tidy tasks completed. So this test is not only shorter, it's far easier to read and it was far easier to write. We're able to tell it a glance what we're doing, all the steps that we're doing in this test. And it'll make it easier for your teammates to contribute to it and to update as things change. So we use the CDK testing API in that example. Let's take a look a little bit more into all of the API methods that's available. The first thing we need to do is get a harness loader for our environment. This is the step that I skipped in that previous example. The English CDK testing library has environments available for unit tests and for ETE tests. And out of the box, it has support for karma for unit tests and protractor for ETE tests. Unfortunately, we know that protractor has gone away, so you'll need to find a different framework to use for ETE testing. If you do use, let's say Cyprus for ETE tests and perhaps just for unit tests instead, then you'll need to find a environment loader that matches the framework that you're using. This would be found from the framework itself or from a third party author. Angular supports out of the box karma and protractor. And in our examples, we'll use karma unit tests. So we don't have to worry about loading an environment loader for a specific library. The first thing we'll do is get the harness loader for the environment. You can set up both the harness loader and the test but harness environment from the Angular CDK library. So next in the test fixture, you want to define a harness loader to use. In your test bed setup, after you've created the component fixture, you'll pass the component fixture into the test bed harness environment loader. Now you have a harness loader that you can use throughout your test. So once you have a harness loader, you can now get harnesses. There are several ways you can do this. The first way is to get an individual harness by calling loader.getHarness and you pass in the type of harness you're looking for. In this case, we're saying map button. So what this will do is return either the first map button that comes across the template or it'll reject the promise. Notice we're using the async away pattern here. You can also get all harnesses of a certain type by calling loader.getAllHarnesses and passing in the type of harness you're looking for. So in this case, I'm getting an array of all map buttons that it has in the template or if there are no map buttons in the template, then I'll get an empty array. Lastly, you can focus your search within a subsection of the component by creating a child loader. So here, I'm creating a child loader and passing in a selector. The selector could be something like nav or footer or it could be a class like I have in my example. Once I create this child loader, then I can load harnesses like we just talked about. You can also filter for specific harnesses if the component harness that you're trying to get supports this and it will be different per component harnesses, very unique. So the map button harness supports searching by text. So now I can load all harnesses for map button that contain the text delete. For all component harnesses, you can access the host element. This is similar to getting the native element from a debug element if you've used that in your component tests. So once you have the host element, which is of type test element, then you can hover, blur, get computed properties, get the class list, things like that. You can also optimize for multiple actions by using a helper method parallel. What parallel does is batches of multiple async operations and also optimizes for change detection. So in this case, in one go, I'm able to get both the whether a checkbox is checked and the label for the checkbox. So we looked a lot at the CDK testing API methods, but we didn't talk about material component harnesses themselves. However, we did use it in our example. So all material component harnesses implement those base CDK testing API and will additionally have a unique API that's based off of the components functionality. For example, matte buttons will have click and we already saw matte checkbox. You can get labels, you can toggle, you can get the checked status, things like that. Luckily, material component API documentation includes the test harness documentation as well. So if you go to material.angler.io and look up the component that you're interested in on the API tab, there is a section for test harnesses. There you can identify what the public API is for interacting with the component as well as how you can search if there's any querying filters available. So we've talked about the CDK testing API as well as implementations of it in material. The really cool thing is you can also implement your own custom component test harness as well using the CDK API. This is particularly helpful if you have a core component that you realize a lot in your application, maybe you have an implementation of a UI control or you just have a component that you want to write a test harness for. Either way you want to go, this is a really powerful tool that we could use. So let's take a look at how to do this by going back to our to-do app. Now we'll look at some of the bells and whistles. We'll look at the top part. So how this works is you put in the task you want to complete, you want to work on. And then these parts are sentiment radar. We're busy people. So we might need to optimize by the satisfaction that we get based off of completing the task. So if you had a task for tidying up unit tasks, it would be a five-part rating. To give you a little bit of background, the component that we'll write the test harness for looks something like this. It's a pretty small component. Of note, we have the selector app sentiment rating and we have the template, which is just a bunch of map buttons. And the map buttons are map icon buttons that are either going to be filled hearts or unfilled hearts based off of the sentiment rating. So if you have a sentiment rating of three, you'll have three filled hearts and two unfilled hearts. So the first thing we want to do is to find the filter interface for support querying because as we already saw, being able to search by particular filter is pretty powerful. To do so, we create an interface sentiment rating harness filters and extend from the base harness filter. Next, we want to identify how we want to search. And in our case, since it's a sentiment rater, it makes sense to search by rate. Next, implement our component harness. We create a class sentiment rating harness and extend off a base class component harness. We also then define the host selector. And this should match the selector of your component that you're writing this test harness for. In our case, it's app sentiment rating. Next, we want to return all the UI elements on this component. And because in our case, we have a fairly simple component that's just a bunch of buttons, we can use the base classes locator for all method and use the selector button. If you have multiple different types of UI elements in here, there are other methods that's in the base class that you could use instead. Now note that we're not returning the buttons themselves. We're returning a function that returns the buttons. And this is done so that it optimizes for the current state of the buttons and for change detection. Next, let's think about the public API for this component. Since this component is a rating component, I expect that most people want to get rate and set rate. So let's implement those next. To implement get rate, we first want to get all the rate buttons. Then we want to return the number of filled hearts, which will correspond with what the rate of this component is. And because we're making a couple of asynchronous calls in this, you can use that helper method parallel. Next to set the rate, we first want to do some error handling because that's just prudent. And we want to get the rate buttons and click on the button that matches the requested rate. So with that, we have a component test harness. But we still want to add the filtering to do that import the interface that we wrote. And then we want to create a static method with which we return a harness predicate. We create a new harness predicate with the option rate, which we want to be able to search on and implement the predicate. And how this work is we're just making sure that the requested rate matches the rate of this component. And with that, now we're completely complete with the component test harness. However, there's one more thing you really should do, and that is you should test your component test harness, which sounds super like test inception, and it's true. However, you're created an API for your components, so you want to treat it as such. In order to test your test harness, you want to create a test host for the component and then write test utilizing your component test harness. If you haven't worked this pattern of creating a test host to test your components, then check out the documentation on Angular.io in their testing section. I hope you're inspired to tidy up your test now. If you'd like to learn more, check out my slides and my tidy task to do app on GitHub. In particular, the tidy task to do app has all of the source code for that tidy task to do, as well as tests with and without harnesses so you can compare how that looks like to cross all different sorts of functionality of the to do app. Additionally, I recommend checking out the excellent documentation on material.angler.io. They have a Angular material component harness guide, so that walk you through how to use component harnesses with Angular material to write tests. And they also have a test harness API in the CDK section if you are a library author or if you want to create an environment loader for your favorite framework. Lastly, I want to hear from you as you tidy up your test or if you have any questions or you just want to talk about testing or if you have any tips. I love talking about tests.