Step 2 - The core functionality
My approach to writing new software is to always do the most important thing first and get it done before I move on to other, less important, functions. What is the core functionality of this application? I would argue that creating an ATM that has some funds is the first thing that we should focus on. If there is no ATM you will not be able to do a withdrawal, right? And it the ATM has no funds you won't be able to get any cash from it either.
The user story:
As a User
In order to make an withdrawal
The ATM needs to have funds
So let's start with creating a ATM
class and assign some funds to each ATM
that we create. You already know a little bit about classes from the Prep Course material.
Since we are working with TDD, we start with creating a test file first.
As a reminder, It is important that we agree on three things at this point.
Your tests/specs are placed in the
spec
folderYour implementation (or production code) are placed in the
lib
folderYour settings (like Gemfile, etc.) are placed in the main project folder
Alright?
First test
Okay, moving on... Create a new file named atm_spec.rb
in your spec
folder.
$ touch spec/atm_spec.rb
Let's add the following test to that file. Note the keywords describe
and it
. Also, as in all ruby programs we are creating blocks with the do
and end
keywords. Make it a habit that you always add an end
if you type do
.
!FILENAME spec/atm_spec.rb
require './lib/atm.rb'
describe Atm do
it 'has 1000$ on intitialize' do
expect(subject.funds).to eq 1000
end
end
Make sure that you run that spec from your terminal.
$ rspec spec/atm_spec.rb
What will follow now is a series of steps that aims at showing you how testing can drive your development. In the future you will probably skip some of this steps but for now, bear with me.
If you examine the terminal output, you'll see a line like this one.
/your/path/atm/spec/atm_spec.rb:1:in `require': cannot load such file -- ./lib/atm.rb (LoadError)
...
That means that the spec file can not load ./lib/atm.rb
(where we are supposed to have our implementation code).
Of course not, we haven't created that file yet. There's no lib
folder yet either. Let's create all that now.
$ mkdir lib
$ touch lib/atm.rb
Run your spec again.
rspec spec/atm_spec.rb
/your/path/atm/spec/atm_spec.rb:2:in `<top (required)>': uninitialized constant Atm (NameError)
A new error message. But not the same as before. That is good. So what have we here? uninitialized constant Atm
? Yes, there is no Atm
class defined. Let's do that.
!FILENAME lib/atm.rb
class Atm
end
Let's have another go at the spec.
$ rspec spec/atm_spec.rb
Atm
has 1000$ on initialize (FAILED - 1)
Failures:
1) Atm has 1000$ on initialize
Failure/Error: expect(subject.funds).to eq 1000
NoMethodError:
undefined method `funds' for #<Atm:0x007f8043fdf2a8>
New error message? Cool!
Yes, there is no method funds
for the Atm
class. Let's add that by adding a attr_accessor :funds
to the class. What is attr_accessor
? You can read about it in this Stack Overflow answer.
!FILENAME lib/atm.rb
class Atm
attr_accessor :funds
end
Another go at the spec and another error message.
$ rspec spec/atm_spec.rb
Atm
has 1000$ on intitialize (FAILED - 1)
Failures:
1) Atm has 1000$ on intitialize
Failure/Error: expect(subject.funds).to eq 1000
expected: 1000
got: nil
Okay, so we expected funds
to be 1000
but it was nil
. Let's make it so that every time an ATM object is instantiated the balance is automatically set to 1000.
We can do that by setting that value in the initialize
method. initialize
is a constructor method that will be run every time an instance of a class is created.
!FILENAME lib/atm.rb
class Atm
attr_accessor :funds
def initialize
@funds = 1000
end
end
And now, when you run RSpec, the test passes.
$ rspec spec/atm_spec.rb
Atm
has 1000$ on initialize
Finished in 0.00195 seconds (files took 0.67858 seconds to load)
1 example, 0 failures
Yay! First success! Green is GOOD!
Lesson learned: Every feature, no matter how small, will lead to a series of failures. Until it doesn't. This goes for new, inexperienced programmers, as well as for those of us who has been doing this for a long time. There's nothing wrong with you. Just get used to it and see everything you do as a learning experience.
Keep your calm, read the error messages that RSpec so kindly throws at you and make small steps forward. Be thankful that you have a testing framework that helps you to figure out what is wrong with your code. Imagine if you were coding without it?
Alright, enough of coding philosophy. Let's move on. This is a great time to do a commit and push up your code (Unless you already did that).
Doing a withdrawal
Let's add another test to the atm_spec
. Inside the describe Atm
block, add this spec.
!FILENAME spec/atm_spec.rb
it 'funds are reduced at withdraw' do
subject.withdraw 50
expect(subject.funds).to eq 950
end
In my spec file that it
block starts at line 7. What I can do is to run JUST that particular block, instead of the entire spec file. It might seem trivial right now, but further down the road we'll have dozens of specs and trust me, you don't want to keep running them all at once.
$ rspec spec/atm_spec.rb:7
Run options: include {:locations=>{"./spec/atm_spec.rb"=>[7]}}
Atm
funds are reduced at withdraw (FAILED - 1)
Failures:
1) Atm funds are reduced at withdraw
Failure/Error: subject.withdraw 50
NoMethodError:
undefined method `withdraw' for #<Atm:0x007fac30e79378 @balance=1000>
...
Shoots! New error. Yes, yes, yes. That is supposed to happen! ;-)
So, we have an undefined method 'withdraw'
. Alright. let's create the withdraw
method and let it take one argument - the amount we want to withdraw from the Atm.
!FILENAME lib/atm.rb
class Atm
attr_accessor :funds
def initialize
@funds = 1000
end
def withdraw(amount)
end
end
Run RSpec just to see another error message.
$ rspec spec/atm_spec.rb:7
Run options: include {:locations=>{"./spec/atm_spec.rb"=>[7]}}
Atm
funds are reduced at withdraw (FAILED - 1)
Failures:
1) Atm funds are reduced at withdraw
Failure/Error: expect(subject.funds).to eq 950
expected: 950
got: 1000
...
Okay, I think you get the point now. We don't have to follow the error messages in such detail from now on.
Let's add some functionality to the withdraw
method that actually adjust the balance
.
!FILENAME lib/atm.rb
class Atm
#...
def withdraw(amount)
@funds -= amount
end
end
And when you run RSpec again, the test passes. Another one bites the dust!
Last updated