Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # TDD is great. It helps me focus on, think through and flesh out the problem before I try and solve it. Without tests, I only have a vague understanding of the problem domain.
- # Well written tests can also serve as documentation for your code - documentation that should never go out of date.
- # 3 rules for unit testing
- # - unit tests should test behaviour
- # the systems we make only exist because they bring some useful behaviour to the end user, whether that is a person, a computer, or another part of the system. therefore we should only be interested in the interface, not implementation. badly designed tests can lead to us exposing the internals of the system solely for the purpose of testing.
- # - unit tests should be descriptive
- # describe the public interface of your code and make it clear to the reader what behaviour is being verified. this allows future you, other developers and non-technical people to understand what the code does.
- # - system under test should be isolated
- # when testing a class, a controller, a router, whatever, the dependencies and state of SUT should be controlled and mocked. only test one behaviour at a time.
- # 4 tips for unit testing
- # - the 4 phase testing pattern
- # SETUP –> EXERCISE –> VERIFY –> TEARDOWN
- # - SETUP - we create a specific state for the system
- # - EXERCISE - we perform the action
- # - VERIFY - we verify the behaviour of the action against our expectations
- # - TEARDOWN - we reset the state of the system
- # identifying the four phases in comments makes the intent of the test much easier to see.
- # EXAMPLE
- describe User, '#save' do
- it 'encrypts the password' do
- user = User.new(password: 'password') #setup
- user.save #exercise
- user.encrypted_password.should_not be_nil #verify
- end
- end
- # Where the same setup phase is repeated in multiple tests, it can be tempting to extract it out to keep your code DRY. But I think in tests, it is better to be as clear as possible as to what is going on, so we don't have to sacrifice clarity or go look elsewhere to find the context of the test.
- # Frameworks often have the teardown phase built in. Database cleaning tools are available with customisable strategies that will allow you to migrate and seed your test db between each test.
- # - be structured as possible - structure your tests by method, and by setup context
- describe ExampleClass '#method1' do
- context 'default' do
- it 'does one thing' do
- # exercise and verify the behaviour of #method1 in the default context
- end
- end
- context 'when some condition applies' do
- it 'does one thing' do
- # set up the conditions for this context
- # exercise and verify the behaviour when some conditions apply
- end
- end
- context 'when some other different condition applies' do
- it 'does one thing' do
- # set up the different conditions for this context
- # exercise and verify the behaviour when some different conditions apply
- end
- end
- end
- describe ExampleClass '#method2' do
- # ...
- end
- # etc...
- # - be descriptive as possible, and make your descriptions as clear as possible
- # giving your tests clear descriptions is like giving your variables, methods and classes good names. they should be as unambiguous as possible and provide as much clarity to the user as possible as to what they do. using the structure above will help.
- describe Counter, '#increment' do
- context 'Default' do
- subject(:counter) { Counter.new }
- it 'increases the count by 1' do
- counter.increment
- expect(counter.count).to eq 1
- end
- end
- context 'when the maximum count is reached' do
- let(:max_count) { 10 }
- subject(:counter) { Counter.new max_count }
- it 'does not increase the count' do
- max_count.times { counter.increment }
- counter.increment
- expect(counter.count).to eq max_count
- end
- end
- end
- # "Counter #increment Default increases the count by 1"
- # "Counter #increment when the maximum count is reached does not increase the count"
- # - Leave out unnecessary details
- describe Person, '#grow_up' do
- context 'Default' do
- it 'increases the persons age by 1' do
- hobbies = [:painting, :music, :cycling]
- person = Person.new('Michael', 26, :green, hobbies)
- person.grow_up
- expect(person.age).to eq 27
- end
- end
- end
- # Why do we need to know that the person is called Michael, likes the colour green, painting, music and cycling?
- # It obscures what we actually care about, which is the age
- # We can extract the Person making to a helper method that provides default values, and only pass in what care about - the age
- describe Person, '#grow_up' do
- context 'Default' do
- it 'increases the persons age by 1' do
- create_person(age: 26)
- person.grow_up
- expect(person.age).to eq 27
- end
- end
- def create_person arguments
- first_name = arguments[:first_name] || 'Michael'
- age = arguments[:age] || 21
- favourite_colour = arguments[:favourite_colour] || :green
- hobbies = arguments[:hobbies] || [:painting, :music, :cycling]
- Person.new first_name, age, favourite_colour, hobbies
- end
- # Now the test is much more to-the-point
- # And by providing default values, if we change the implementation later and add in another initialization parameter to the Person class, we only have to change it in one place - the create_person helper method.
- # We are not taking the person initialization - the setup phase - away from the test, we are just changing how the setup is done, and making it clearer to the reader what is being tested
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement