Original link:
https://blog.nrwl.io/rxjs-advanced-techniques-testing-race-conditions-using-rxjs-marbles-53e7e789fba5


This article is
RxJS Chinese communityTranslated articles, if need to be reproduced, please indicate the source, thank you for your cooperation!


If you would like to translate more quality RxJS articles with us, please click here
[here]

Victor Savkin is co-founder of NRWL.io. He previously worked on the Angular core team at Google and built dependency injection, change detection, forms modules, and routing modules.

Building a Web application involves multiple back-end, Web Workers, and UI components, all of which update the state of the application concurrently. It is easy to introduce bugs due to the competitive state. In this article I’ll show you examples of such a bug, how to use RxJS Marbles to expose the problem in a unit test, and how to fix it eventually.

I use Angular and RxJS, but anything I’m going to talk about really applies to any Web application, regardless of the framework used.

The problem

Let’s look at ‘MovieShowingsComponent’. It is a simple component for showing movie screenings. When the user selects a movie, the component immediately displays the selected movie name, and then, once the response is received from the back end, the corresponding ‘Showings’ is displayed.

@Component({
  selector: 'movie-showings-component'.
  templateUrl: './movie-showings.component.html'
})
export class MovieShowingsComponent {
  public movieTitle: string;
  public showings: string[];

  constructor(private backend: Backend) {}

  selectMovie(movieTitle: string) {
    this.movieTitle = movieTitle;

    this.backend.getShowings(movieTitle).subscribe(showings = > {
      this.showings = showings;
    });
  }
}
Copy the code

There are race conditions for this component. To see the problem, imagine the following scenario. Suppose the user selects’ After the Storm ‘and then’ Paterson ‘. It should look something like this:

Let’s assume here that the ‘After the Storm’ response will return first. But what if it’s not? What happens if ‘Paterson’ returns the response first?

The app actually says’ Paterson ‘but it says’ Showings’ is’ After the Storm’ and the app is broken.

Before we start fixing it, let’s write unit tests to expose this race condition. Of course, there are many ways to do this. But since we use RxJS, we use Marbles, a tool for testing concurrent code that is powerful and has a lot of potential.

Marbles

To use Marble, we need to have the Jasmine marbles library installed.

npm install - save-dev jasmine-marbles
Copy the code

Before writing tests for components, let’s take a look at how Marble tests typically work by testing the concat operator.

import {cold. getTestScheduler} from 'jasmine-marbles';
import 'rxjs/add/operator/concat';

describe('Test'. (a) = > {
  it('concat'. (a) = > {
    const one$ = cold('x-x|');
    const two$ = cold('-y|');

    expect(one$.concat(two$)).toBeObservable(cold('x-x-y|'));
  });
});
Copy the code

Here we create two observables using the ‘cold’ helper provided by Jasmine marbles: ‘one$’ and’ two$’. (If you’re not familiar with hot and cold observables, read this article by Ben Lesh.)

Next, we use the concat operator to get the result Observable, which we compare to the expected result.

Marbles is a domain-specific language used to define RxJS Observables. Using it we can define when observables emit values, when they are idle, when they report errors, when they are subscribed, and when they are finished. In our tests, we define two observables, one of the (‘ x – x | ‘) issued a “x”, and then wait for 10 milliseconds from another ‘x’, and then to complete. The other waits 10 milliseconds before saying the ‘y’.

It is usually not enough to emit a single letter string. The ‘cold’ helper function provides a way to map it to other objects, like this:

import {cold. getTestScheduler} from 'jasmine-marbles';
import 'rxjs/add/operator/concat';

describe('Test'. (a) = > {
  it('concat'. (a) = > {
    const one$ = cold('x-x|'. {x: 'some value'});
    const two$ = cold('-y|'. {y: 999});

    expect(one$.concat(two$)).toBeObservable(cold('a-a-b|'. {a: 'some value'. b: 999}));
  });
});
Copy the code

As with many DSLS, we used Marbles to improve the readability of our test code. Marbles does this really well, and we only have to glance at the test code to see what the test code is doing.

If you want to learn more about Marbles testing, check out this video.

Test competition conditions

With this powerful tool, let’s write unit tests to expose competition conditions.

import { MovieShowingsComponent } from './movie-showings.component';
import { cold. getTestScheduler } from 'jasmine-marbles';

describe('MovieShowingsComponent'. (a) = > {
  it('should not have a race condition'. (a) = > {
    const backend = jasmine.createSpyObj('backend'. ['getShowings']);
    const cmp = new MovieShowingsComponent(backend);

    backend.getShowings.and.returnValue(cold('--x|'. {x: ['10am']}));
    cmp.selectMovie('After the Storm');

    backend.getShowings.and.returnValue(cold('-y|'. {y: ['11am']}));
    cmp.selectMovie('Paterson');

    // This clears all observables
    getTestScheduler().flush(a);

    expect(cmp.movieTitle).toEqual('Paterson');
    expect(cmp.showings).toEqual(['11am']); // This will fail because showings is ['10am'].
  });
});
Copy the code

Repair competition condition

Let’s look at our component again.

@Component({
  selector: 'movie-showings-component'.
  templateUrl: './movie-showings.component.html'
})
export class MovieShowingsComponent {
  public movieTitle: string;
  public showings: string[];

  constructor(private backend: Backend) {}

  selectMovie(movieTitle: string) {
    this.movieTitle = movieTitle;

    this.backend.getShowings(movieTitle).subscribe(showings = > {
      this.showings = showings;
    });
  }
}
Copy the code

Each time the user selects a movie, we create a new isolated Observable. If the user clicks twice, we have two observables that don’t coordinate. This is the root of the problem.

Let’s change that by introducing an Observable that calls all ‘getShowings’.

@Component({
  selector: 'movie-showings-cmp'.
  templateUrl: './movie-showings.component.html'
})
export class MovieShowingsComponent {
  public movieTitle: string;
  public showings: string[];

  private getShowings = new Subject<string>(a);

  constructor(private backend: Backend) {
  }

  showShowings(movieTitle: string) {
    this.movieTitle = movieTitle;
    this.getShowings.next(movieTitle);
  }
}
Copy the code

Next, we map observable to showings list.

@Component({
  selector: 'movie-showings-cmp'.
  templateUrl: './movie-showings.component.html'
})
export class MovieShowingsComponent {
  public movieTitle: string;
  public showings: string[];

  private getShowings = new Subject<string>(a);

  constructor(private backend: Backend) {
    this.getShowings.switchMap(movieTitle = > this.backend.getShowings(movieTitle)).subscribe(showings = > {
      this.showings = showings;
    });
  }

  showShowings(movieTitle: string) {
    this.movieTitle = movieTitle;
    this.getShowings.next(movieTitle);
  }
}
Copy the code

In this way, we replace a collection of isolated observables with a single high-order Observable, to which we can apply synchronization operators. The synchronization operator refers to switchMap.

The ‘switchMap’ operator will subscribe only to the latest calls to ‘backend.getShowings’. If another call is executed, it unsubscribes to the previous call.

With this change, our test will pass.

The source code

You can find the source code in this repository. Note the “skipLibCheck” in the tsconfig.spec.json file: true.

conclusion

In this article, we looked at an example of a bug caused by a race condition. We used Marbles, which is a powerful way to test asynchronous code to expose bugs in unit tests. We then refactor the code to fix this bug by using a single high-order Observable with the switchMap operator applied.

Victor Savkin is co-founder of Nrwl, an enterprise Angular consulting firm.

If you liked it, go to 💚 below, so others can see it on Medium, too. Follow @victorSavkin to read more about Angular.