Marble testing is an expressive way to test observables by utilizing marble diagrams. This lesson will walk you through the syntax and features, preparing you to start writing marble tests today!
Marble testing allows for an expressive way to test observable sequences in your application. To get started, there's a couple of helper files we need, and we can grab those from the RxJS library. You can find these under RxJS Spec in Helpers. The main files we're concerned about are this marble-testing.ts and test-helper.ts.
Once we have those files included in our project, we'll need to add them to your favorite test runner setup phase. If you're using Wallaby.js, that'll be in the setup function, and if you're using Karma, that'll be in the pre-processor phase.
What that will do is supply us some helper methods, set up and tear down a test scheduler in between tests, and allow us to parse marble diagrams as observables. Let's get started.
We'll start by creating two basic marble diagrams, just to see how the pieces fit together. We can create our source and expected result that for now will match the source.
Let's go ahead and line up source and inspect it for clarity. Next we can call expectObservable and assert that the source will equal our expected result.
There's a couple of things to notice here, and the first is this expectObservable method. This hangs out the test scheduler and is exposed globally by the test-helper file we included at the beginning of this video.
At the beginning of each test, the new test scheduler is created and then destroyed once the test is complete. This gives us a clean slate between tests.
The next thing to notice is that our test is actually failing right now, as you can see by these red dots. Why is that? ExpectObservable actually expects this source to be an observable, and right now, it's just this marble diagram. How can we fix that?
There are two helper methods we have access to that will turn this marble diagram into an observable. They are Hot and Cold. Cold will treat the beginning of the diagram as a subscription point, where Hot will actually let you identify the subscription point yourself, which we'll see a bit later.
Let's go ahead and wrap our source in the Cold method, and we should now see our test passing once we do. What's actually happening here?
Each of these dashes stands for 10 frames, so in this case, we have 0, 10, 20, emit A at 30 frames, 40, 50, 60, emit B at 70 frames, and then 80, 90, 100. This pipe actually stands for complete, so we'll complete at 100 frames.
Behind the scenes, test messages will be created for each emission and significant event, and will ultimately be compared to the emissions from this expected observable. To see this in action, let's go ahead and make this test fail. If we pull up the console, we can see that each emission includes the frame, the kind, which n for next, and the value, which is just the string A.
Since we removed one dash from here, we could expect this is 10 frames shorter on the complete, so if we come down here, we can see that this completed at frame 100, when we actually expected it to complete at frame 110.
Now that we understand how this works, let's see some different ways that we can model observables with marble testing. Let's fix our first test. Next, we'll create two Cold observables. Our first will emit A, and then B, and then complete, and our second will emit C, and then D, and then complete.
What we're going to test here is a concat operator. What we would expect to happen is for A and then B to be emitted, and then on completion, C and then D to be emitted, and then complete.
Let's call our expectObservable method and expectObservable1.concatObservable2 to be the expected result. This is where marble testing really shines over a typical unit testing of observables, as we can more clearly see the end result of concatting observable1 and observable2, and each emission along the way.
Next, let's take a look at how we can model Hot observables in marble testing. We'll go ahead and create two observables similar to what we had before, but instead of the Cold method, we'll use the Hot method.
What this does is allow us to model a Hot observable at the point at which a subscription would occur, and that is done through this caret operator. In this case, A is emitted and C is emitted before the subscription, so our end result is B and D.
We can actually test these subscription points separately if we wish, and that is done through the expect-subscriptions helper method. This method accepts the subscriptions object that is added to all of our observables under test, as well as a diagram describing what our subscription point and unsubscription point should be.
In this example, we are concatting observable2 to observable1. What we would expect is once observable1 completes, observable2 will be subscribed to until ultimately completing.
We can model this in our subscription diagram using the caret and exclamation mark. The caret symbolizes the start of the subscription for observable1. When observable1 completes, this is the unsubscribe point. Observable2 will then be subscribed to, marked by this caret, and when observable2 completes, this is the unsubscribe point.
To see how this works, we can make this first test fail. We'll remove one dash so the unsubscribe frame is 10 less, and now we can go ahead and pull up our test console and see that behind the scenes, these subscription objects are being kept with the subscribe frame and unsubscribe frame. Now, our unsubscribe frame is 60, where it really should be 70.
There's a couple of other important features to be aware of when using marble testing. The first is the ability to control what values are being emitted. So far, we have just used basic characters for our observables under test, but imagine we wanted to use numbers or objects, or maybe even arrays.
Marble testing allows us to pass in an object map to both the Cold and Hot method as a second parameter, as well as a second parameter to the toBe method. When these tests are run, these values will be subbed in for their corresponding spots in the diagram.
To see this, we can make this test fail and pull up our test console. You can see now, the values are 1 and 2 instead of A and B. Next is the ability to model observables that emit multiple values in the same frame.
One example of this is observable.of, which emits the provided values in sequence. To model this, we simply wrap our values here in paren's, and these will all be treated as if they were emitted in the same frame.
Lastly, we need to model errors that may occur with our observables. In this example, we have an observable that emits one, two, and three. Each time the value becomes greater than two, we throw an error.
We also added the Retry operator to repeat this two times before finally giving up. How do we model this behavior? You can see down here that we have A, B, and then we're going to retry once, retry twice, and the key here is this pound sign. The pound sign represents an error in marble diagrams.
The last thing to be aware of is we can actually test that the right error is thrown, and we can do this by adding a third parameter to the toBe methods. Here we are throwing an error that says, "Number 2 high," and in our toBe method, as a third parameter, we're supplying this string, "number 2 high."
That is RxJS marble testing.