We write tests to cover scenarios for the functionality of our code. There are also other advantages to writing a better test. We could make the test as a documentation for the code. Following a simple pattern for writing rspec, we will be able to write test faster and we will start loving to write tests.
[Step 1] Describe
While writing any test the first thing you write is the describe block. Let’s suppose we are writing test for a method called play inside a JukeBox class. First describe block will contain the class name. Describe block helps you know what class/method you are writing rspec for.
describe JukeBox do end
The next thing you write in describe is the method name
describe JukeBox do describe ‘.play’ do end end
Note: If your class has just a single method and no more methods will be added in that class you can skip the method describe part.
[Step 2] Context
Once the description about the method for which you are writing the test is there the next step would be to define the context. A method could work differently for different contexts.
Let’s add two contexts for the method.
- When a user has inserted coin
- When a user has not inserted coin
describe JukeBox do describe ‘.play’ do context 'when user has inserted coin' do end context 'when user has not inserted coin' do end end end
While writing context, think about all the possible cases that could come in.
[Step 3] Scenario
Now inside each context, write the scenario what it should be doing. In our jukebox when a coin is inserted the jukebox’s play method should return ‘a beautiful song for you’ and when the user has not inserted a coin the jukebox should return ‘no money no song :]’. So let’s add them.
describe JukeBox do describe ‘.play’ do context 'when user has inserted coin' do it 'return song being played message' do end end context 'when user has not inserted coin' do it 'return insert coin message' do end end end end
[Step 4] Subject
The subject is the main part of the test. Write down how you make the method call in a subject. Put the subject at the top so that you can reuse it across different context.
describe JukeBox do describe ‘.play’ do subject(:play_music) { described_class.new(coin).play } context 'when user has inserted coin' do let(:coin) { Coin.new(10) } it 'return song being played message' do end end context 'when user has not inserted coin' do let(:coin) { FakeCoin.new(10) } it 'return insert coin message' do end end end end
Notice that I have added coin variable inside each context since the only thing that changes in these two contexts is the coin. If you do not define the coin variable it will throw an error that the variable coin is not defined since it is in the used in the subject of the test.
[Step 5] Mock/Stub
Now mock/stub any external calls that you don’t need to cover test for. There might be some other methods being called, or any external API calls or even database calls. Let’s say this play method uses validate_coin method to know if the coin is valid or not.
describe JukeBox do describe ‘.play’ do subject(:play_music) { described_class.new(coin).play } context 'when user has inserted coin' do let(:coin) { Coin.new(10) } before do allow(JukeBox).to_receive(:validate_coin) .with(coin).and_return(true) end it 'return song being played message' do end end context 'when user has not inserted coin' do let(:coin) { FakeCoin.new(10) } before do allow(JukeBox).to_receive(:validate_coin) .with(coin).and_return(false) end it 'return insert coin message' do end end end end
[Step 6] Expectation
Now everything is set. The only thing remaining is to write the actual expectation.
describe JukeBox do describe ‘.play’ do subject(:play_music) { described_class.new(coin).play } context 'when user has inserted coin' do let(:coin) { Coin.new(10) } before do allow(JukeBox).to_receive(:validate_coin) .with(coin).and_return(true) end it 'return song being played message' do expect(play_music).to return('a beautiful song for you') end end context 'when user has not inserted coin' do let(:coin) { FakeCoin.new(10) } before do allow(JukeBox).to_receive(:validate_coin) .with(coin).and_return(false) end it 'return insert coin message' do expect(play_music).to return('no money no song :]') end end end end
All set. The test is now ready.
[Step 7] Refactor
The test is now running well. There seems to be some code that would be reused. So let’s use a variable and reuse the before each block.
describe JukeBox do describe ‘.play’ do subject(:play_music) { described_class.new(coin).play } before do allow(JukeBox).to_receive(:validate_coin) .with(coin).and_return(valid) end context 'when user has inserted coin' do let(:coin) { Coin.new(10) } let(:valid) { true } it 'return song being played message' do expect(play_music).to return('a beautiful song for you') end end context 'when user has not inserted coin' do let(:coin) { FakeCoin.new(10) } let(:valid) { false } it 'return insert coin message' do expect(play_music).to return('no money no song :]') end end end end
With this, there will be a fixed pattern of writing tests. If all developers across the organization follow the same structure it will be easy for all the developer to modify the code that someone else has written. Also if a developer wants to use a method that is already there, tests helps to give the context what is the expected response of that method and what are the parameters that it takes.
Are you trying to learn a new programming language? Try learning golang from zero to hero