|
| 1 | +# Testing |
1 | 2 |
|
| 3 | +When designing any system, you also want to test it to guarantee its quality and that this quality does not regress as the system is modified throughout its lifetime. Ideally, you want to automate the process of testing it. Modern software is backed by thorough unit tests and Rx code should be no different. |
| 4 | + |
| 5 | +Testing a synchronous piece of Rx code is as straight-forward as any unit test you are likely to find, using predefined sequences and [inspection](/Part 2 - Sequence Basics/3. Inspection.md). But what about asynchronous code? Consider testing the following piece of code: |
| 6 | +```java |
| 7 | +Observable.interval(1, TimeUnit.SECONDS) |
| 8 | + .take(5) |
| 9 | +``` |
| 10 | +That is a sequence that takes 5 seconds to complete. That means every test that uses it will also take 5 seconds or more. That's not convenient at all if you have thousands of tests to run. |
| 11 | + |
| 12 | +## TestScheduler |
| 13 | + |
| 14 | +The piece of code above isn't just time consuming, it actually wastes all that time waiting for time to pass. If you could fast-forward the clock, that sequence would be evaluated almost instantly. Well, you can't fast-forward your system's clock, but Rx sees time through its schedulers. You can have a scheduler that virtualises time, called `TestScheduler`. |
| 15 | + |
| 16 | +The `TestScheduler` does scheduling in the same way as the schedulers that we saw in the chapter about [Scheduling and threading](/Part 4 - Concurrency/1. Scheduling and threading.md). It schedules actions to be executed either immediately or in the future. The difference is that time is frozen and only progresses upon request. |
| 17 | + |
| 18 | +### advanceTimeTo |
| 19 | + |
| 20 | +As the name suggests, `advanceTimeTo` will execute all actions that are scheduled for up to a specific moment in time. That includes actions scheduled while the scheduler was being fast-forwarded, i.e. actions scheduled by other actions. |
| 21 | + |
| 22 | +```java |
| 23 | +TestScheduler s = Schedulers.test(); |
| 24 | + |
| 25 | +s.createWorker().schedule( |
| 26 | + () -> System.out.println("Immediate")); |
| 27 | +s.createWorker().schedule( |
| 28 | + () -> System.out.println("20s"), |
| 29 | + 20, TimeUnit.SECONDS); |
| 30 | +s.createWorker().schedule( |
| 31 | + () -> System.out.println("40s"), |
| 32 | + 40, TimeUnit.SECONDS); |
| 33 | + |
| 34 | +System.out.println("Advancing to 1ms"); |
| 35 | +s.advanceTimeTo(1, TimeUnit.MILLISECONDS); |
| 36 | +System.out.println("Virtual time: " + s.now()); |
| 37 | + |
| 38 | +System.out.println("Advancing to 10s"); |
| 39 | +s.advanceTimeTo(10, TimeUnit.SECONDS); |
| 40 | +System.out.println("Virtual time: " + s.now()); |
| 41 | + |
| 42 | +System.out.println("Advancing to 40s"); |
| 43 | +s.advanceTimeTo(40, TimeUnit.SECONDS); |
| 44 | +System.out.println("Virtual time: " + s.now()); |
| 45 | +``` |
| 46 | +Output |
| 47 | +``` |
| 48 | +Advancing to 1ms |
| 49 | +Immediate |
| 50 | +Virtual time: 1 |
| 51 | +Advancing to 10s |
| 52 | +Virtual time: 10000 |
| 53 | +Advancing to 40s |
| 54 | +20s |
| 55 | +40s |
| 56 | +Virtual time: 40000 |
| 57 | +``` |
| 58 | + |
| 59 | +We scheduled 3 tasks: one to be executed immediately, and two to be executed in the future. Nothing happens until we advance time, including tasks scheduled immediately. When we advance time, tasks are executed from the queue, until a task that is not ready yet. |
| 60 | + |
| 61 | +You can even set time to a previous moment than the one you are now. That isn't a useful feature as much as a source of bugs in your tests. Usually, `advanceTimeBy` will be closer to what you want to do. |
| 62 | + |
| 63 | +### advanceTimeBy |
| 64 | + |
| 65 | +`advanceTimeBy` advances time relative to the current moment in time. In every other regard, works like `advanceTimeTo`. |
| 66 | + |
| 67 | +```java |
| 68 | +TestScheduler s = Schedulers.test(); |
| 69 | + |
| 70 | +s.createWorker().schedule( |
| 71 | + () -> System.out.println("Immediate")); |
| 72 | +s.createWorker().schedule( |
| 73 | + () -> System.out.println("20s"), |
| 74 | + 20, TimeUnit.SECONDS); |
| 75 | +s.createWorker().schedule( |
| 76 | + () -> System.out.println("40s"), |
| 77 | + 40, TimeUnit.SECONDS); |
| 78 | + |
| 79 | +System.out.println("Advancing by 1ms"); |
| 80 | +s.advanceTimeBy(1, TimeUnit.MILLISECONDS); |
| 81 | +System.out.println("Virtual time: " + s.now()); |
| 82 | + |
| 83 | +System.out.println("Advancing by 10s"); |
| 84 | +s.advanceTimeBy(10, TimeUnit.SECONDS); |
| 85 | +System.out.println("Virtual time: " + s.now()); |
| 86 | + |
| 87 | +System.out.println("Advancing by 40s"); |
| 88 | +s.advanceTimeBy(40, TimeUnit.SECONDS); |
| 89 | +System.out.println("Virtual time: " + s.now()); |
| 90 | +``` |
| 91 | +Output |
| 92 | +``` |
| 93 | +Advancing by 1ms |
| 94 | +Immediate |
| 95 | +Virtual time: 1 |
| 96 | +Advancing by 10s |
| 97 | +Virtual time: 10001 |
| 98 | +Advancing by 40s |
| 99 | +20s |
| 100 | +40s |
| 101 | +Virtual time: 50001 |
| 102 | +``` |
| 103 | + |
| 104 | +### triggerActions |
| 105 | + |
| 106 | +`triggerActions` does not advance time. It only executes actions that were scheduled to be executed up to the present. |
| 107 | + |
| 108 | +```java |
| 109 | +TestScheduler s = Schedulers.test(); |
| 110 | + |
| 111 | +s.createWorker().schedule( |
| 112 | + () -> System.out.println("Immediate")); |
| 113 | +s.createWorker().schedule( |
| 114 | + () -> System.out.println("20s"), |
| 115 | + 20, TimeUnit.SECONDS); |
| 116 | + |
| 117 | +s.triggerActions(); |
| 118 | +System.out.println("Virtual time: " + s.now()); |
| 119 | +``` |
| 120 | +Output |
| 121 | +``` |
| 122 | +Immediate |
| 123 | +Virtual time: 0 |
| 124 | +``` |
| 125 | + |
| 126 | +### Scheduling collisions |
| 127 | + |
| 128 | +There is nothing preventing actions from being scheduled for the same moment in time. When that happens, we have a scheduling collision. The order that two simultaneous tasks are executed is the same as the order in which they where scheduled. |
| 129 | + |
| 130 | +```java |
| 131 | +TestScheduler s = Schedulers.test(); |
| 132 | + |
| 133 | +s.createWorker().schedule( |
| 134 | + () -> System.out.println("First"), |
| 135 | + 20, TimeUnit.SECONDS); |
| 136 | +s.createWorker().schedule( |
| 137 | + () -> System.out.println("Second"), |
| 138 | + 20, TimeUnit.SECONDS); |
| 139 | +s.createWorker().schedule( |
| 140 | + () -> System.out.println("Third"), |
| 141 | + 20, TimeUnit.SECONDS); |
| 142 | + |
| 143 | +s.advanceTimeTo(20, TimeUnit.SECONDS); |
| 144 | +``` |
| 145 | +Output |
| 146 | +``` |
| 147 | +First |
| 148 | +Second |
| 149 | +Third |
| 150 | +``` |
| 151 | + |
| 152 | +## Testing |
| 153 | + |
| 154 | +Rx operators involving asynchronous, schedule those actions using a scheduler. If you take a look on all the operators in [Observable](http://reactivex.io/RxJava/javadoc/rx/Observable.html), you will see that such operators have overloads that take a scheduler. This is the way that you can supplement their real-time default schedulers for your `TestScheduler`. |
| 155 | + |
| 156 | +Here is an example where we will test the output of `Observable.interval` against what we expect it to emit. |
| 157 | + |
| 158 | +```java |
| 159 | +@Test |
| 160 | +public void test() { |
| 161 | + TestScheduler scheduler = new TestScheduler(); |
| 162 | + List<Long> expected = Arrays.asList(0L, 1L, 2L, 3L, 4L); |
| 163 | + List<Long> result = new ArrayList<>(); |
| 164 | + Observable |
| 165 | + .interval(1, TimeUnit.SECONDS, scheduler) |
| 166 | + .take(5) |
| 167 | + .subscribe(i -> result.add(i)); |
| 168 | + assertTrue(result.isEmpty()); |
| 169 | + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); |
| 170 | + assertTrue(result.equals(expected)); |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +This is useful for testing small, self-contained pieces of Rx code, such as custom operators. A complete system may be using schedulers on its own, thus defeating our virtual time. [Lee Campbell suggests](http://www.introtorx.com/Content/v1.0.10621.0/16_TestingRx.html#TestingRx) abstracting over Rx's scheduler factories (`Schedulers`), with a provider of our own. When in debug-mode, our custom scheduler factory will replace all schedulers with a `TestScheduler`, which we will then use to control time throughout our system. |
| 175 | + |
| 176 | + |
| 177 | +### TestSubscriber |
| 178 | + |
| 179 | +In the test above, we manually collected the values emitted and compared them against what we expected. This process is common enough in tests that Rx comes packaged with `TestScubscriber`, which will do that for us. It collectes every notification received. With `TestSubscriber` our previous test becomes: |
| 180 | + |
| 181 | +```java |
| 182 | +@Test |
| 183 | +public void test() { |
| 184 | + TestScheduler scheduler = new TestScheduler(); |
| 185 | + TestSubscriber<Long> subscriber = new TestSubscriber<>(); |
| 186 | + List<Long> expected = Arrays.asList(0L, 1L, 2L, 3L, 4L); |
| 187 | + Observable |
| 188 | + .interval(1, TimeUnit.SECONDS, scheduler) |
| 189 | + .take(5) |
| 190 | + .subscribe(subscriber); |
| 191 | + assertTrue(subscriber.getOnNextEvents().isEmpty()); |
| 192 | + scheduler.advanceTimeBy(5, TimeUnit.SECONDS); |
| 193 | + subscriber.assertReceivedOnNext(expected); |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +A `TestSubscriber` collects more than just values and exposes them through the following methods: |
| 198 | + |
| 199 | +```java |
| 200 | +java.lang.Thread getLastSeenThread() |
| 201 | +java.util.List<Notification<T>> getOnCompletedEvents() |
| 202 | +java.util.List<java.lang.Throwable> getOnErrorEvents() |
| 203 | +java.util.List<T> getOnNextEvents() |
| 204 | +``` |
| 205 | + |
| 206 | +There are two things to notice here. First is the `getLastSeenThread` method. A `TestSubscriber` checks on what thread it is notified and logs the most recent. That can be useful if, for example, you want to verify that an operation is/isn't executed on the GUI thread. Another interesting thing to notice is that there can be more than one termination event. That goes against how we defined our sequences in the begining of this guide. That is also the reason why the subscriber is capable of collecting multiple termination events: that would be a violation of the Rx contract and needs to be debugged. |
| 207 | + |
| 208 | +`TestSubscriber` provides shorthands for a few basic assertions: |
| 209 | +```java |
| 210 | +void assertNoErrors() |
| 211 | +void assertReceivedOnNext(java.util.List<T> items) |
| 212 | +void assertTerminalEvent() |
| 213 | +void assertUnsubscribed() |
| 214 | +``` |
| 215 | + |
| 216 | +There is also a way to block execution until the observable that the `TestSubscriber` is subscribed to terminates. |
| 217 | +```java |
| 218 | +void awaitTerminalEvent() |
| 219 | +void awaitTerminalEvent(long timeout, java.util.concurrent.TimeUnit unit) |
| 220 | +void awaitTerminalEventAndUnsubscribeOnTimeout(long timeout, java.util.concurrent.TimeUnit unit) |
| 221 | +``` |
| 222 | + |
| 223 | +Awaiting with a timeout will cause an exception if the observable fails to complete on time. |
2 | 224 |
|
3 | 225 |
|
4 | 226 | #### Continue reading |
|
0 commit comments