Unit Testing ComponentStore with Spectator

Jason Warner
ngconf
Published in
13 min readSep 10, 2021

This is the final part of this series on the @ngrx/ComponentStore. Part 1 and Part 2 were an introduction to using the ComponentStore. Part 3 was a quick introduction to Spectator. The code for this part can be found in this repository from GitHub.

When unit testing our ComponentStore applications, an important consideration is what tests provide the most value. Remember that every test you write is code that has to be maintained and has a cost for your company or project. We want our tests to be useful and not test the ComponentStore itself. It is well tested and we don’t need to make sure it works as advertised. We don’t want our tests to be brittle and break on simple changes. We also don’t want our unit tests to fall into the grey area between integration tests and unit tests. All of these considerations require a bit of thought.

Let’s start with the PersonStore. This is our store that provides state for our application. It inherits from ComponentStore<T>. A good starting point to decide what to test might be to test anything that has logic in it. For selectors, we might want to test anything that has a .pipe() associated with it. This is a good idea, but let’s consider something like the editedPerson$ selector. For reference, here is the code:

This selector definitely has a pipe attached to it, but all it does is log to the console. I have unit tested selectors that only log to the console in the past. We need to consider how important is this console.log() really? I would contend that unit testing this selector is fine and I would not reject a pull request that did so, but you might be forgiven for looking for a test that will provide more value for your time.

So what do we test in our selectors? I submit that testing any selectors with logic is worth your time. I would hope that you don’t have too many selectors with logic in them. I find that too many selectors with logic often point to flaws in my state object. There are cases where we do need logic in our selectors. In those cases, we should definitely write tests for that logic.

Testing Updaters

A very valuable target for testing store objects are updaters. Updaters take some data and update the state of the ComponentStore based on that data. A broken updater will break the entire application. This means that time verifying updaters is time well spent. One example is the loadPeople() updater.

This updater takes an array of Person objects or null . If an array of Person objects is passed in, then the state is updated to put that array in the people property. If null is passed in then an empty array should be in the state for the people property. As a bonus, to verify that the loadPeople() updater is working, we need to check the people$ selector and see what it emits. Let’s look at some tests for this updater.

This is a pretty standard Spectator service test. On line 10, we use the createServiceFactory() method. However, notice line 12. This is one of my favorite things about Spectator. The built in mocking allows us to skip over a lot of the boilerplate involved in creating and injecting mock services into our tests. By adding a type to the mocks array in the object passed to createServiceFactory(), Spectator will create a Jasmine mock (or a Jest mock if you use the Jest imports) and inject that into the providers array for you. This is incredibly powerful and lets you focus on the tests. One thing to remember is that the mocks array will only automatically create mocks for methods on the type you pass in. For a lot of services this is enough. As a spoiler, this distinction will become important in the future when we need to mock properties for our tests.

The first test for the loadPeople() updater is on line 24. It checks that when loadPeople() is called with an array of Person objects, that it properly puts them into the state. We do this by using the RxJS scan() operator. This operator is similar to the reduce() method for arrays. It allows you to have an accumulator that you can add to. In our tests, we simply add to an array (of type Person[][]). The reason for using the scan() operator instead of the RxJS reduce() operator is because the reduce() operator waits until an observable completes before emitting. The scan() operator emits the accumulator after every emit of the source observable. In our case, we do not want the people$ selector to complete. This would break our ComponentStore .

In these examples, we use the same scan() code over and over again in our tests. Usually, it is a good idea to create your own RxJS operator if you are going to be using the same code over and over in your pipe() methods. It is really simple. Netanel Basal has a really great article on it here. We won’t be doing that in this article because I want the tests to be obvious. In my day to day coding, I have created a collectEmits() operator based on this code and is something I highly recommend.

Notice that we set up our subscription before our call to loadPeople() on line 34. This order is important. We want to make sure that we are ready to handle the emits from the ComponentStore before we start interacting with it. This will allow us to catch everything that is emitted by the people$ selector.

Line 36 is where we check that the emitted values are what we expect. We check against [[], people] because our scan() code is adding everything emitted to the emits variable and our subscribe() is placing that in the allEmits variable. Our selector is going to emit [] initially because it is empty and then it will emit people from the loadPeople() call on line 34. By capturing everything, we can build a timeline from our selector and test complex interactions if needed.

The next test is on line 39. This test makes sure that if null is passed to loadPeople() that an empty array is placed in the people property of the state. This is to prevent null from potentially breaking component templates that rely on this array. This test is almost an exact copy of the previous test except for two changes.

The first change is on line 50. We added loadPeople(null) to the updater calls. You don’t have to leave the original call of loadPeople(people). I left it to better demonstrate how the scan() operator was working in our tests.

The second change is on line 52. We change the check to [[], people, []] . This is because of the initial empty array, the change to people after the first updater call and then the change to an empty array from being passed null in the updater. If you did not leave in the initial loadPeople(people), then we would expect the emits to be [[], []].

Updater tests are pretty easy to write. As an added bonus updater tests often require us to verify selectors at the same time.

Testing Effects

Effects are valuable to test because effects are how your store responds to something happening in your application. Effects almost always have valuable business logic to test. Effects can be a source of difficult to reproduce bugs if they don’t behave as expected.

We will create a simple test for the editPerson() effect. This effect expects the ID of the person to edit and then sets the editorId and the editedPerson properties in the state. In our example, we will only test the happy path where the ID passed in matches a person in our people array of the state. As an exercise, you can add tests for when the ID is undefined or doesn’t match an ID from the people array on the state. Think about what should happen and write tests for those cases. Don’t write tests for what the code says.

Here is the code for the happy path test:

These tests are very similar to the updater tests. The way we verify that our code is correct is to interact with the selectors that we created for our store. In this case, it is the editorId$ selector and the editedPerson$ selector. To make things easier, I added a beforeEach() for these tests that loads the people array into the store before each test. This allows us to focus on what needs to be tested in the current test.

This test has a familiar strategy. First we set up a scan() to collect what is emitted from our selectors on line 13 and line 19. Next, we call our effect. In this test it is a call to editPerson(1) on line 24. After we act on our service, we verify our expectations on lines 26 and 27. Again, we capture all of the emits from our selectors. This allows us to verify the timeline of the state of the PersonStore. In complex situations, this is very valuable information to have.

Testing effects on a ComponentStore is often very easy. Testing effects is valuable because we are testing our effect logic to prevent future bugs. We also get the added benefit of verifying the output of the related selectors.

Testing Additional Methods on the ComponentStore

The final area of our PersonStore that will be valuable to test are any methods that we added that aren’t selectors, updaters or effects. In our case, they are the clearEditedPerson() and saveEditPerson() methods.

We are going to examine the happy code path for the saveEditPerson() method. Testing this method will require us to use the mock added to the mocks array. As an exercise, I encourage you to think of what other tests should be written for both methods and then write them yourself. Remember, think of what the tests should do, not what the code says.

To test the saveEditPerson() method, we need to know the expected behavior for the happy path. When saveEditPerson() is called, the editorId and the editedPerson from the state object are sent to StarWarsApiService.savePerson() . If the service returns successfully, we then call the updatePerson() effect with the value returned from the API call. After that, we clear the editedPerson and editorId properties on the state. Let’s see what these tests could look like:

There is a bit of code here, but it is pretty simple. We created three tests for a very good reason. We want to isolate and test specific behaviors. For this reason, we have a test to make sure that StarWarsApiService.savePerson() is called with the proper values. We have a test that makes sure that the value returned from the API is properly added to the store. Finally, we have a test to make sure that editorId and editedPerson are properly cleared out. We could have combined all of this into one mega test, but if there is a failure, we have more work to determine what failed. Let’s look at some of the key lines of code.

In the test to make sure StarWarsApiService.savePerson() is called with the proper values, we need to get a reference to the mock of the StarWarsApiService. In Spectator, we can simply call spectator.inject(StarWarsApiService). Spectator.inject() gives us access to the inject() function from the TestBed. We do this on line 14.

After we have the reference, we need to make sure the mock returns a value so we don’t break the rest of the code in our test. For our first test, we merely return the special EMPTY observable. This observable emits nothing and completes immediately. This way our code that expects an observable doesn’t break in our PersonStore. We do this on line 16.

The last thing we do in our first test is use the toHaveBeenCalledWith() matcher to make sure that our mock method was called with the proper values. We have now verified that our PersonStore properly interacts with our API.

Now let’s examine the two tests for when the API returns successfully. In these tests, we need to simulate the API returning a good response. To do this, we get a reference to the mock in the beforeEach() on line 33. This time on line 35, we return an observable of(apiResponse). apiResponse is a copy of the Person object that we are “editing” in our tests but with a small change. By making this change, we can make sure that the data in the store changes to what is returned by the mocked service.

When we test that the store updates the people property properly, we only subscribe to the people$ selector instead of using a scan() operator like the other tests. The reason for this is that we really only care about the last value emitted by this selector after a successful API response. The intermediate values are not as interesting in this test. You can see this on line 41 of the example code.

Finally, in our test to make sure the editorId and editedPerson are properly reset, we capture all of the values for the two selectors. This was a conscious decision. If those selectors never change, then we may have an issue in our code. We can look at the values between the start of our test and when those values are set to undefined to make sure our code behaves as expected.

Notice that we only have two elements in our expected values on lines 71 and 72. The reason for this is that by the time our scan() and subscribe() are added, we have already set the editorId and editedPerson in the beforeEach() using the editPerson() effect on line 10. This means we won’t capture the inital undefined. That initial value isn’t really important to our test, so we don’t worry about it. The real test is to make sure we change from the values being edited to undefined again.

Spectator is a powerful tool that makes testing our ComponentStore classes really easy. These same principles could be applied to TestBed tests, but by using Spectator, we can cut down on our boilerplate and focus on the important aspects of our tests.

Testing Components That Rely on a ComponentStore

How do we test classes that rely on our PersonStore? We could just inject the PersonStore into our components and our tests would work. If we inject the PersonStore, we begin to straddle that grey space between unit testing and integration testing. By isolating our tests using a mock of the PersonStore, we can take better control over our tests. It is important to do integration testing, so it is up to you and your team to decide what is the best course of action to achieve your goals.

Assuming that mocking the PersonStore is the course of action to take, we need to decide how to test our components. The simplest tests are to verify that updaters or effects are called with the appropriate parameters at the right times.

To consumers of PersonStore, our updaters and effects look like simple method calls. Unfortunately, the way our updaters and effects are defined they are properties on PersonStore. This means we cannot simply put PersonStore in the mocks array. The documentation for Spectator says that it will not automatically mock properties on an object in the mocks array. Don’t worry! We can simply use the mockProvider() helper ourselves. This is the same helper that the mocks array uses to provide automatic mocks for us.

Here is how is an example from tests for the EditPersonComponent:

By using the second parameter of mockProvider(), we can provide an object that has the properties we want to add to our mocked service. We do this on line 31. personStoreStub is an object that has the properties we want for our mock. Using a stub object allows us to use the Partial<T> utility type from TypeScript to get type-safety for the object we pass to mockProvider.

We use the jasmine.createSpy() helper to create a spy for the setEditedPerson property on line 26. We can then use the toHaveBeenCalledWith() matcher on line 48 of this example to make sure that the effect is called with the proper data. Testing a ComponentStore with this strategy allows us to verify that our components interact with the PersonStore in the manner we expect. We can easily write tests for effects and updaters using this strategy.

Another potential problem to overcome is testing components that rely on selectors. How do we mock observable properties on our ComponentStore objects when testing a component in a manner that we can easily control? I have a strategy that I use. An example of this strategy can be used for the PersonListComponent. Here is an example:

This time we are going to focus on just the key pieces of code and not the entire test file. We first create a Subject<T> for the selector we want to mock. It is important that we use Subject<T> here because we do not want values emitted from one test to pollute tests that come afterwards. This can lead to test runs that seem to randomly fail or worse cover bugs because of the way we created our mocks. The rest of creating the mock is exactly like the previous example. We use the mockProvder() helper and pass our stub object as the second parameter.

Next we have the tests that use this set-up.

This test is really contrived, but demonstrates how a test could work. By using the Subject<T> that we created earlier, we can emit values from our service in a controlled manner. This also demonstrates why the Subject<T> is important. The only time the values emitted matter is if there is a subscriber at the time editorId$ emits. This is why we set up our subscription on line 4 before we emit on lines 8 through 10.

Using this strategy, we can set up mocks of our PersonStore that have strongly-typed mocks that match the interface of PersonStore. If anything changes, then our tests will break. We also have a very fine-grained control over when an observable emits that will not pollute tests that run after the current test.

In this article, we explored how to test services that inherit from ComponentStore<T>. We also explored possible ways to test components that rely on those services. We used Spectator in these tests to help reduce a lot of the boilerplate by taking advantage of powerful mocking tools Spectator provides. All of the code from this article is available on GitHub. Thank you for reading this series. It has been a lot of fun to work on.

Now that you’ve read this article and learned a thing or two (or ten!), let’s kick things up another notch!
Take your skills to a whole new level by joining us in person for the world’s first MAJOR Angular conference in over 2 years! Not only will You be hearing from some of the industry’s foremost experts in Angular (including the Angular team themselves!), but you’ll also get access to:

  • Expert panels and Q&A sessions with the speakers
  • A friendly Hallway Track where you can network with 1,500 of your fellow Angular developers, sponsors, and speakers alike.
  • Hands-on workshops
  • Games, prizes, live entertainment, and be able to engage with them and a party you’ll never forget

We’ll see you there this August 29th-Sept 2nd, 2022. Online-only tickets are available as well.
https://2022.ng-conf.org/

--

--

Jason Warner
ngconf
Writer for

I enjoy everything related to code and being a dev. However, my only skills are showing up and being lucky and I'm not sure if luck is a talent.