Slide 1

Slide 1 text

Test asynchronous functions with RSpec 4IJHFSV/BLBKJNBBLB!MFETVO -VYJBSDP MUE 3VCZ,BJHJ

Slide 2

Slide 2 text

Summary I’m talking about how to write RSpec test codes for asynchronous functions

Slide 3

Slide 3 text

My Experience I implemented asynchronous functions for the RoR application to support WebSocket. I was confusing to test asynchronous functions with RSpec.

Slide 4

Slide 4 text

synchronous functions We have wrote synchronous functions for web applications for a long time.

Slide 5

Slide 5 text

Why we can wrote synchronous functions? Web servers create a thread per received HTTP request. Server keeps receiving next requests. We can treat received request synchronously. We don't have to care subsequent requests.

Slide 6

Slide 6 text

Why does WebSocket require asynchronous functions? Now, many web browsers supports WebSocket. To support it, web application needs asynchronous functions. Why? Because we can’t create threads automatically for WebSocket messages. Why?

Slide 7

Slide 7 text

Threads per messages bring C10K problems We send and receive messages on the WebSocket connections. Type of messages and amount of messages depend on the application. If we want to send a big message in split chunk. Number of chunks may be 10 thousands. Can we create threads per messages? They will spent huge amount of memory.

Slide 8

Slide 8 text

Event loop model architecture We know "Event loop model architecture" is a nice solution for the C10K problem. They have only one thread and the control flow is switched at waiting IO(Input/Output).

Slide 9

Slide 9 text

Ruby has EventMachine The EventMachine gem provides event loop model architecture. “faye-websocket-ruby” or other gems for WebSocket use it.

Slide 10

Slide 10 text

Event loop model brings a new problem Event loop model has only one thread. If you spent long time in one function, all subsequent events will wait until finishing it. We MUST NOT run slow functions.

Slide 11

Slide 11 text

Asynchronous functions If we need to run slow functions, we create a thread by myself and run the function in it. We wait end of it and send messages on the WebSocket connection. This is an asynchronous function.

Slide 12

Slide 12 text

Sample implementation of asynchronous function • Create a new thread • Run slow calculation • Return result by calling block def async_func Thread.start do yield slow_caluculation end end

Slide 13

Slide 13 text

Use this function with faye-websocket-ruby def call # Create a WebSocket connection. ws = Faye::WebSocket.new(env) ws.on :open do async_func do |result| # Send results on the WebSocket connection. ws.send result end end ws.rack_response end

Slide 14

Slide 14 text

We can write asynchronous functions We can support the WebSocket. Next:

Slide 15

Slide 15 text

Can we test asynchronous functions? RSpec is a major testing framework Use RSpec here

Slide 16

Slide 16 text

RSpec `async_func` Does this spec success? it 'is expected answer' do async_func do |result| expect(result).to eq('expected answer') end end

Slide 17

Slide 17 text

RSpec `async_func` Does this spec fail? it 'is expected answer' do async_func do |result| expect(result).not_to eq('expected answer') end end

Slide 18

Slide 18 text

No, the spec don’t fail

Slide 19

Slide 19 text

Why does not spec fail? RSpec captures failed assertions by exception. But, this assertion runs in sub thread. it 'is expected answer' do async_func do |result| # --- in sub thread --- expect(result).not_to eq('expected answer') # --- in sub thread --- end end

Slide 20

Slide 20 text

OMG! Tests that never fail are not helpful.

Slide 21

Slide 21 text

How can we fail this spec? An important thing is “Assertions should run in the RSpec thread”. Ruby has `Queue` to wait other threads.

Slide 22

Slide 22 text

Collect assertion Spec fails expectedly! it 'result is details' do queue = Queue.new async_func do |r| # --- in sub thread --- queue.push r # --- in sub thread --- end # Wait to `queue.push` from other threads. result = queue.pop # Resume next code when `queue.push` is called. expect(result).not_to eq('expected answer') end

Slide 23

Slide 23 text

We can write asynchronous functions. We can support the WebSocket. We can write test codes for asynchronous functions.

Slide 24

Slide 24 text

Let’s Play Async!

Slide 25

Slide 25 text

AsycPlay gem To avoid boilerplate code, I created 'async_play' gem. https://rubygems.org/gems/async_play it 'result is details' do result = AsyncPlay.opening do |curtain| async_func do |val| curtain.call val end end expect(result).to eq('expected answer') end