GO TO:
RxJS observables
RxJS is a library for creating, managing and handling asynchronous data streams in JavaScript applications. The key element of RxJS is observables, which represent the data streams emitted over time.
Observables are based on the Observer design pattern, in which the observer “subscribes” to the observed object and waits for the data it emits. RxJS offers a variety of operators, such as map, filter, and merge, which allow you to transform, filter, and merge data streams. The concept of the RxJS library is described in more detail in the article “Reactive Programming with JavaScript in RxJS“.
What is RxJS testing (marble testing in RxJS)?
RxJS enables reactive programming in JavaScript, so you can support asynchronous data streams and manipulate them with various operations, such as mapping, filtering, and merging. RxJS testing is designed to ensure that the code running on these streams is correct, error-free, and meets expectations.
Marble testing
In the context of RxJS testing, it is worth mentioning marble testing, which is one of the most popular tools for testing data streams in this library. Marble testing is a visualization of data streams using so-called marble diagrams. In marble, data streams are represented as strings of characters, where each character symbolizes a value in the stream or a special event, such as the end of the stream or the occurrence of an error. Marble testing allows you to define the expected results and compare them with the actual test results, which is especially useful for complex asynchronous operations.
Test types
RxJS testing, especially using marble diagrams, allows you to verify whether functions and reactive operators work correctly in the context of unit tests. Unit tests focus on the evaluation of individual functions, operators and data streams. Their goal is to isolate individual code elements and check whether they behave as expected.
RxJS Testing Tools
To facilitate testing with marble diagrams, RxJS provides tools such as TestScheduler, which allows you to control time virtually without affecting your actual test run time. TestScheduler allows you to define input values and expected output values using marble diagrams, and then simulates data flow in reactive streams.
How to write RxJS test cases?
Writing RxJS test cases requires careful planning, an understanding of the source code, and the use of suitable testing tools. Marble testing is one of the approaches that helps to create transparent tests for data streams. Here are some tips on how to write tests for code based on the RxJS library:
1. Get to know the source code
Before writing test cases, carefully read the source code that will be tested. Understanding the structure of the code and the purpose of individual functions and operators will allow for the creation of effective and comprehensive tests.
2. Choose the right testing framework
Choose a JavaScript testing framework, such as Jasmine, Mocha or Jest, that best suits your needs and preferences. Make sure that the chosen framework works with RxJS and allows you to use marble testing if you plan to take advantage of this approach.
3. Create the test structure
Unit tests are usually organized in “describe” blocks (or similar, depending on the framework you use) which serve grouping tests for a specific function, operator, or component. In the “describe” blocks, we place individual test cases, defined by the “it” function (or the equivalent in the selected framework).
4. Apply marble testing
Marble testing is an approach that allows you to visualize data streams using so-called marble diagrams. In marble testing, data streams are represented as strings of characters, where each character symbolizes a value in the stream or a special event, such as the end of the stream or the occurrence of an error.
For RxJS testing using marble testing, use tools provided by the library, such as TestScheduler. TestScheduler allows you to control time in a virtual way and define input values and expected output values using marble diagrams.
5. Define test cases
Test cases should be defined in a legible and understandable manner, describing the expected behavior of a function, operator or data stream. Using marble testing, you can accurately and legibly describe the expected behavior of data streams and compare the actual results with the expected ones.
6. Test different cases and scenarios
When writing test cases, it is worth testing different scenarios, such as:
- Data flow with different values
- Data flow with delays or intervals
- Code behavior in case of errors
- End of data streams
- Proper transfer of values to subsequent operators
- Also, try to include boundary cases to make sure the tested code can resist different situations.
7. Browse and improve test cases
Review your test cases regularly to make sure they are up-to-date and are still working. Try to optimize your tests by eliminating repetitive or redundant pieces of code, as well as making it easier to read and understand. This will facilitate the maintenance and development of tests as the source code changes.
Writing RxJS test cases, especially using marble testing, requires an understanding of the structure of the code, selection of the appropriate framework for testing, creation of a test structure, and definition of the test cases that accurately describe the expected behavior of data streams. By applying the above guidelines, you will be able to create clear and effective unit tests for your code based on the RxJS library.
Elevate Your Application Development
Our tailored Application Development services meet your unique business needs. Consult with Marek Czachorowski, Head of Data and AI Solutions, for expert guidance.
Schedule a meetingWhat is a marble diagram in RxJS?
Marble diagrams are visual representations of data streams in the RxJS library that help you understand the flow of data and interactions between operations in streams. The introduction of marble diagrams into the RxJS software development process can improve code readability, facilitate communication between developers, and speed up the testing implementation process.
How to read marble diagrams in RxJS?
Marble diagrams use characters to represent the values emitted by the stream, the end of the stream, the occurrence of an error, and subscription as well as the cancellation thereof. Here are some additional tips on how to read diagrams:
Time: Each hyphen (–) means a unit of time, also called a “frame”. In marble tests, the length of the time frame is defined in the TestScheduler tool, which allows you to control the time of a test.
Values: alphanumeric characters (e.g., a, b, c) represent the values emitted by the data stream. These values are usually defined in a value object passed to the toBe() method, which maps these characters to actual values.
Subscribing and unsubscribing: in Marble, we can use the symbol ^ to indicate the moment when the subscription starts, and ! to indicate the moment when it is cancelled.
An example of a marble diagram for RxJS may look like this:
---a---b---c---|
This diagram represents a data stream that emits three values: a, b, c, each emitted at equal time intervals. The last symbol, |, indicates that the stream was finalized after emitting c.
A B C map(x => x.toUpperCase()) A B C
In this case, we have an input stream that emits the values a, b, c. Then we used the map operator, which converts each emitted value into its equivalent with a capital letter. The resulting stream thus emits the values of A, B, C.
Marble diagrams can be much more complicated, with many operators and events. It is vital to understand basic notation to interpret and create such diagrams.
What are the types of marble tests in RxJS?
There are two main types of Marble tests in RxJS: unit tests and integration tests.
Unit tests
Unit tests focus on isolating and testing individual components, operators or functions. Marble testing is very useful in unit testing as it allows you to precisely check the sequence of events and the effects of individual operators on data streams. In unit tests, we use TestScheduler, which allows us to control the time and verify the expected test results.
Integration tests
Integration tests test the interactions between different components, operators or functions in the system. Marble testing can also be used in integration tests to examine how different parts of the code work together. In integration tests, we check whether the data flow between different components is in accordance with our expectations. Marble testing in the context of integration testing is more complex than unit testing, but it can provide valuable information about the behavior of the system as a whole.
When testing with marble diagrams, it is also important to understand what “hot” and “cold” operators are. In the context of data streams, a hot operator is one that emits values regardless of whether there is currently any subscription. In turn, the “cold” operator begins to emit values only after subscription. In marble tests, the “cold” operator is usually represented by streams marked as source. Let’s picture this using the example below:
import { TestScheduler } from 'rxjs/testing'; import { of, Subject } from 'rxjs'; import { map, takeUntil } from 'rxjs/operators'; it('should demonstrate hot and cold observables', () => { const testScheduler = new TestScheduler((actual, expected) => { expect(actual).toEqual(expected); }); testScheduler.run(({ cold, hot, expectObservable }) => { const sourceCold$ = cold(' --a--b--c--|'); const sourceHot$ = hot(' --d--e--f--|'); const stop$ = cold(' -----s--|'); const expectedMarble = ' --x--y--(z|)'; const resultCold$ = sourceCold$.pipe( takeUntil(stop$), map(((value) => value.toUpperCase()) ); const resultHot$ = sourceHot$.pipe( takeUntil(stop$), map(((value) => value.toUpperCase()) ); expectObservable(resultCold$).toBe(expectedMarble, { x: 'A', y: 'B', from: 'C' }); expectObservable(resultHot$).toBe(expectedMarble, { x: 'D', 8. from: 'F' }); }); });
In the example above, we see two data streams: “hot” and “cold”. The “cold” stream only starts emitting values after the subscription, while the “hot” stream emits values regardless of subscription. In the test, we use the “takeUntil” operator to stop observing data streams at the right time.
To sum up, marble diagrams in RxJS are extremely helpful in understanding the data flow and behavior of individual operators in data streams. Marble testing facilitates the creation of unit and integration tests, which are necessary for the proper functioning of the application based on RxJS. Know-how on marble diagrams and the ability to write effective marble tests are important competencies for every programmer who uses the RxJS library.
When working with marble, it is worth remembering a few additional rules and tips that can facilitate the testing process:
- Naming conventions: When you create data streams using marble diagrams, it’s a good idea to maintain naming conventions and use names related to stream functionality. This allows us to better understand which data flows through the streams, making it easier to analyze the test results.
- Using Helper: In many cases in marble testing, we can use support functions provided by the RxJS library, such as TestScheduler, hot, cold, time, expectObservable and expectSubscriptions. These features make it easy to create and manage marble diagrams and test the flow of data between different streams.
- Verification of test results: When writing marble tests, it is worth paying attention to test results and errors reported by testing tools such as Jasmine or Mocha. This allows us to quickly identify and correct errors in the code.
- Accuracy of tests: Marble testing allows for precise tracking of the sequence of events in data streams. However, it is important to remember that marble testing does not replace other types of testing, such as unit or integration testing. It is worth using different testing techniques depending on the context and needs of the application.
- Practice: Marble testing may be difficult to understand at first, but over time and with experience it becomes much easier. Working with different examples and scenarios will help you to understand how to best use marble diagrams in RxJS tests.
Using marble in RxJS, you can effectively test data streams and interactions between operations. Marble diagrams make it easier to understand the data flow and the expected test results, which leads to building more reliable and efficient applications. Practice and experience are key to mastering marble testing, but its benefits are priceless to any front-end developer.
Read also: Agile tester roles and responsibilities
What are the benefits of RxJS testing?
- The code performs as expected – testing the RxJS code is extremely important because it allows you to increase the level of certainty that the code is working as you expect. The tests enable developers to be sure that their applications meet the assumed business requirements, which translates to a lower risk of errors or inconsistencies.
- Error detection – at the same time, testing makes it easier to detect errors, because it checks individual functions, operators and the interactions between them. This allows you to identify and fix problems in the code more quickly. It also shortens the debugging time, as the tests help developers locate the source of the error quickly.
- It’s easier to make changes – RxJS tests also support making changes to the code, as they automatically check the functionality after each modification. Thanks to this, the programmer can quickly detect and fix any problems resulting from the introduced changes. In addition, the tests facilitate refactoring, because the programmer can be sure that his changes will not result in regression.
- Code documentation – it is also worth mentioning that RxJS tests often serve as documentation for the code, providing information on how to use individual functions and what their expected results are. Such documentation is invaluable to other programmers who wish to understand someone’s code and familiarize themselves with it.
- Greater trust in software – finally, testing increases trust in code among both developers and project stakeholders. Everyone can be sure that the application works properly and meets the requirements. Tests are crucial to the maintenance of high quality applications based on data streams and reactive programming.
- Better code – as a result, testing the RxJS code contributes to better code quality, faster error detection and correction, easier changes and increased trust in the code. As an RxJS expert, I strongly recommend using tests in all projects based on this library.
FAQ
-
What are the features of RxJS test frameworks and how to choose the best one?
RxJS test frameworks are tools for creating, organizing, and executing tests for RxJS-based code. Choosing the best framework depends on the needs of the project and the team’s preferences. When choosing a test framework for RxJS, it is worth paying attention to the following aspects:
Marble Testing Support: It’s important to choose a framework that supports marble testing, as it is crucial in testing reactive data streams.
Integration with other tools: See how well the framework integrates with other tools used in the project, such as bundlers, CI/CD environments, and linters.
Ease of use: the testing framework should be easy to configure and use.
Community Support: Popular test frameworks are more likely to actively develop and gain the support of the community.
Documentation: good documentation is crucial for the rapid introduction of the test framework into the project and for understanding its capabilities.
Performance: The performance of the test framework impacts the duration of the tests.
Flexibility and expandability: The chosen framework should offer flexibility in configuration and the option to expand it to include additional tools or features.
Examples of popular test frameworks supporting RxJS include Jasmine, Jest, and Mocha. Each of them offers different functions and advantages, so it is worth carefully analyzing their capabilities before deciding which to use. -
How to implement RxJS testing?
To implement RxJS testing in your project, you must first choose the right test framework that supports marble testing, such as Jasmine, Jest, or Mocha. The decision should depend on the needs of the project and the preferences of the team. Then proceed to install and configure the selected test framework using a package manager such as NPM or yarn and follow the instructions provided by the framework documentation.
If the selected test framework does not support marble testing directly, install additional tools such as rxjs-marbles for Mocha or jest-marbles for Jest. Now you can start writing unit tests for RxJS-based components, services, operators, and streams. Use marble tests to represent and test reactive data streams. Make sure you test the key scenarios, both positive and negative, to verify that the code is working properly.
It is a good idea to put the tests in dedicated files or folders so that they can be easily found and managed. The accepted standard is to place tests in the folder __tests__ or in files with the extension .spec.ts or .test.ts. To run the tests, use the tools provided by the test framework, both locally and on the CI/CD server. Depending on the framework, you can use CLI commands such as npm test, yarn test or ng test.
Monitor test results, analyze errors, and fix code issues. Use reporting and statistical tools to assess code quality and track testing progress. As the project grows, update and maintain tests by writing new tests for new features and updating existing tests in case of code changes. This will make it easier to maintain high quality code. -
What are the most common RxJS testing errors?
One such mistake is not taking the principle of asynchrony into account when testing. When testing data streams, it is important to remember to use appropriate asynchronous testing techniques, such as marble testing, or using the fakeAsync and tick functions provided by Angular.
Another common mistake is failing to properly manage your subscriptions. If they are not cancelled once the tests are completed, this may lead to memory leaks and false positive test results. It is worth taking the time to manage subscriptions properly, especially when testing components or services.
Another common mistake is the misuse of RxJS operators, which can lead to unexpected results. It is especially important to understand the difference between a switchMap operator and a mergeMap operator and other operators that influence the behavior of data streams.
Inadequate error testing can lead to a false sense of security. It is worth paying attention to error handling testing, especially for asynchronous operations such as HTTP requests. In such cases, it is worth using the appropriate operators to handle errors such as catchError and retry.
Inaccurate testing of boundary conditions and extreme scenarios can lead to a failure to detect problems. Therefore, it is worth striving for full coverage with tests, as well as studying different combinations of input data that can affect the operation of data streams. Testing these cases may reveal potential issues that you may not notice when testing only typical scenarios.
The last mistake, albeit a no less important one, is the improper use of hot and cold operators in marble testing. It is worth understanding the differences between these operators and knowing when to use them to get reliable test results. -
What are the design patterns of RxJS testing?
RxJS testing design patterns are proven techniques used in testing applications based on this library. A key element is the implementation of marble tests, which allow the visualization and testing of data streams, as well as the analysis of interactions between them. Thanks to the use of such patterns, asynchronous tests will become more effective, and isolating the tested parts of the application from external dependencies will allow you to control behavior and test various scenarios.
-
What are the best testing practices?
When it comes to best testing practices in the context of the RxJS library, it is worth focusing on approaches and techniques that make testing effective, efficient and easy to maintain. Asynchronous testing, including async, fakeAsync or marble tests, allows you to test various scenarios related to asynchronous operations. It is also crucial to test error handling to see if they are properly served in data streams. It is important to test boundary conditions and extreme scenarios, which allows you to detect potential problems that you may not notice when testing only typical scenarios. Monitoring the level of test coverage and taking care of the good organization of tests, placing them in dedicated files or folders, makes it easier to manage the code and maintain its transparency.
Consult your project directly with a specialist
Book a meetingRead also: What is regression testing