Skip to main content

7 Open Source Vlog - Writing Rspec Tests

Episode 81 · July 20, 2015

How do we go about writing robust tests with rspec for simple_calendar?

Testing


Transcripts

What's up guys? This is episode what? 16 of the vlog? This is pretty awesome, we've made so much progress and now it's time to add some tests for our ruby gem simple calendar. We have a bunch of test that we wrote up as we were going along, kind of thinking about what tests are we actually going to need, and a lot of this is evolved now that we have the calendar code available to us. Let me just open these up here, and let's take a look at what we've got. We've got two public methods, and that means that these two methods are ones we certainly want to test and the private methods are ones that are a little less important to test, but we want to make sure that they are appropriately tested so that the public methods don't break, and the reason for that is because those public methods call all those private methods, so we have partial name, we have date_range, start_date, sorted_events, all of those are being used in our code somewhere, so we obviously want to write tests for those, but if you were to write more of an integration level test, a higher level test that would just do the two renders, that could work. You could write a test that says: hey, when we instantiate a calendar let's make sure that we test that it prints out a calendar onto the page. You could do the full integration test, and make sure that the html that gets printed out is what you want, and what you expect. You could definitely do that or we could do a little bit more finer grained and do unit tests. I think I'm going to do unit test for this because the higher level tests are going to be more important if we ever make those views a little bit more complicated. So long as we don't screw those up much, they're really simple, but they're a little less important. We need to make sure that our partial name is converting the class name to a file name appropriately, we need to make sure that the attribute returns the correct values. I'm going to make sure that sorted events does the right thing, we need to make sure that these pieces work, and the view is a little bit more flexible because everybody is going to customize it. That is kind of where we're at. We're testing, we want to make sure that the stuff that should always work in one specific way, we can unit test those, because that's what they're designed to do, just very straightforward stuff when it comes to these things that are going to be a little bit more maleable, like what happens after the render happens, well you should have html on your page, this should return a string of html at least, and that is one of those things that's a little harder to unit test or you tie yourself in really closely with unit test and that becomes more complicated. Let's figure out what we need to do here. One of the more complicated pieces that we've got to deal with her is the view context. We're actually using this to represent the view, so in our test we're going to want to make sure that this is either nil or a test double so we can call methods on it sand just ignore what they do, but sometimes we use that view context call like a method render, and that's going to be important that we make sure that we call that method and this is one of those pieces that becomes the bane of testing, you have to make these fake variables because you don't have the real thing, and that's where testing kind of goes off the rails right now. That is where you spend most of your time dealing with those little things, like look I don't really have a real gem application in gem tests, so we have to fake some of that, and you end up writing a lot of code just to set things up. Ideally if you can get by with not doing much of the fake rails set up, as before, that's the way that you want to write your tests. Let's just dive in and write some simple tests here. I think the easiest things that we can do is test this partial name here. That's going to be most obviously the easiest one, in theory. We actually should look if we have any of our tests in here and see if we have a render test. It doesn't look like we do. Let's go up here and say:

spec/calendar_spec.rb

describe SimpleCalendar::Calendar do 
    it 'renders a partial with the same name as the class' do 
        expect(SimpleCalendar::Calendar.new(nil).send(:partial_name)).to eq("calendar")
    end 

That's what we expect, and we should be able to run our spec and see if that works. We get a failed test and we get failed test and we get "wrong number of arguments (0 for 1..2)". It doesn't look like what we've written in our tests has anything wrong with this, we should be able to run bin/console we get the same error, so actually our issue is with this. So our rspec syntax is fine but if we create a new calendar object... Aha! That's important, we forgot to pass in the view context in the options, and here we can pass in a nil because we don't need the view context here (snippet of code on top already includes it). We set that variable and we don't need a view at all because we're just looking at the class name, and the underscore of that, so we know that we're running the correct tests because it failed and that's good and if we run rspec again, we're going to get the same thing because I forgot to save the file, run it again and this time we get a different one. This is important, because I wanted to make sure that I failed, but I didn't actually mentioned that. We actually want to make sure that our test failed first, and you could test that by doing equal to and empty string just so long as you get a red test so you know that what you're doing is providing the correct output so that you can verify that it's doing the right thing. Because of the module that we're using, the simple_calendar module that we're using, the underscore method converts that to the module here and then the slash to represent the folder name and then the calendar after that so this is what our actual tests should look like and if we run our rspec for that, we get a passing test way up here at the top before all of our pending tests. That's great, we have our first test working and that's all we really need. We can do some things here, we could do a

spec/calendar_spec.rb

describe SimpleCalendar::Calendar do 
    let(:calendar) { SimpleCalendar::Calendar.new(nil)}

    it 'renders a partial with the same name as the class' do 
        expect(SimpleCalendar::Calendar.new(nil).send(:partial_name)).to eq("calendar")
    end 

That allows our tests to be a little bit shorter and then within all of these tests we can just access the calendar variable here and that will let us set it up just this one time in one place and not have to do that every single test. We get the same output, we got a passing test again and we can use that as we go through. Let's also take a look at this attribute one, this is another one where we could write a test for this calendar but in this case we need to pass in some options here so we need two tests here. We need first one to test that it has a default, and then we need another that we can override that attribute, the default attribute for the meetings, so let's do that. Let's make a context:

context 'event sorting attribute' do 
    it 'has start_time as the default attribute' do 
        expect(calendar.send(:attribute)).to eq(:start_time)
    end 
    it 'allows you to override the default attribute' do 
        expect(SimpleCalendar::Calendar.new(nil, attribute: :starts_at).send(:attribute)).to eq(:starts_at)
    end
end 

For each of these tests we really just want to instantiate a new calendar and we'll just copy this, we can actually use the calendar from above. That should allow us to have two tests that basically handle both options for that event situation, and as you can see, both of those are correctly passing, which is great. That's really all we need. This attribute method, we need to make sure that it overrides this accordingly, and it defaults to start_time it's really all we're doing, which is nice and that takes care of these two pieces. So it takes care of a section of the render block because we're calling the partial name, and it also takes care of somewhat of assorted events, because now in sorted events we don't have to make any tests for this, we can assume that that's going to return the correct value and once we write a test for that one, we can go ahead and just kind of trust that it works. The sorted events method is going to be a little bit more complicated, we're going to need to create a fake class with that attribute or struct of some sort so that we have a start time or a starts at, and we need to make sure that that is handled and sorted and converted into an array and convert it into a hash organized by dates. What we'll probably do with that one is we'll have a context

context 'converts an array of events to a hash sorted by days' do 
end 

Another thing you could do is describe and you could do the method. There's a whole different ways to define your tests and it really doesn't matter so long as you run the tests. It's one of those things where rspec is great but there's so many people arguing about should you use context, should you use 'describe', it blocks, expect this to equal that, or could it just be an assert or just you know do a regular ruby evaluation. This is one of those things where people get really opinionated on and I personally like using minitests because it's just straight ruby rather than all the extra things that you can do in rspec. You can't easily refactor rspec code out before capybera or some of those old thins because it's not just the regular old ruby that you're used to, so it makes things a little bit more complicated but by all means, simple is better, that's all I'm looking for. I'm happy to use rspec and just keep it simple. I'm going to leave these as a pending thing. Let's actually change this to

describe "#sorted_events" do 
    it 'converts an array of events to a hash sorted by days' do 
end 

We'll have that as our pending test here, and I'm just going to jump down to the start date date range and additional_days and we'll handle those rather than dealing with sorted events yet, I think we'll tackle that tomorrow because it's going to take a little bit more setup than we would expect. Another thing you might have noticed is we haven't done any tests for view_context or options, so we've just kind of handled this set up in our tests, but we've never explicitly tested the initialize method. We can add tests for it, but at the same time, is it going to make that much difference? We take two variables as arguments? We assign them directly to two variables, there's no magic that happens here, if this ever changes our tests will probably break, we can write a tests for it but you have to ask yourself: How much is too detailed? Because we don't want to have to struggle with a test all the time, we have plenty more valuable things to do with our time in the future. Let's jump into describing these last three maybe just two methods actually, I don't really feel like we need to test additional_days as much because it only applies to this calendar and it actually gets covered if we handle the date_range, that's something that's interesting that we can do here and kind of skip that piece which is nice. Here if we have to test our start_date method, another thing we need to do, is the same as the default attribute for the calendar attribute name, we need to test the default, and we need to test the overriding.

Here we can just say it can defaults to today's date. and we need to use double quotes on that one, and then the other one "it uses the params start date to override it"? This is not very descriptive, I'm not thinking clearly right now. Maybe we can clean up the description for that test because it literally says what it means but it's not useful in the context of what does this start_date do? The start_date is designed so that we know where to calculate the calendar from so that you know can go between different views. That is a better way of describing it but at the same time that's really what the date_range calculates, so we're going to actually need to jump from here to the date_range and test more around this. Really the start date doesn't do anything except it defaults to the current day or uses the one in params and the date range is the one that actually does all the magic and we need to test that more importantly than anything else. That one is where things actually happen. In this case we have a calendar, and we can send the start_date method name, we expect this to equal date.today and this is all fine and dandy but when we run our tests we're going to experience the expected problem...

We've got this test but the problem is our view context is nil and view context doesn't have a method called params, so what are we going to do about that? One of the easiest things that we can do is make a fake class to represent something inside your test and you can use that to override things and simulate rails functionality but in your tests. So we can actually have a method called params here, and what if we just have an attribute, and it's called start_date. This is going to be a helper class that's going to allow us to pass in a view context, so if have view_context.new we can pass in any date that we want, and this would allow us to simply do that. This is going to allow us to call this method, it will hit our fake params and return the start date, which is going to be completely empty. We need to make sure params returns a hash because that is what rails does, so if there's a start date we want start_date = @start_date and if not we just want to return an empty hash. This is actually the correct code that we want for that. If you've given us one and we do present if you like so it's a little more clear to read. If there's one we'll return that in the hash, if there's not we wont, simple as that. That should simulate the hash so if there is no start date in the url, in the params hash then it shouldn't get passed over, and that should allow us to run this test again and undefined method attribute this time.

Now let's run this test, and we get a failing test. Wrong number of arguments 0 for 1. Now this crashed on calendar_spec line seven which is interesting. So it came here. Initialize, and I got to start date and wrong number of arguments. Oh, that is because we have to pass in something here, so let's set this to nil by default and then this will allow our test to run because that will now be optional. That is awesome, that allows us to have a fake view context, it allows our view context in our code to just fake itself and actually works, but this is the work that sucks about writing tests, you have to fake all this stuff that rails provides in order to make your tests run, so you're not working against the real code, and obviously there's better ways to do this, but I'm wanting to point this out, that this is regular old ruby that we're doing and we're cheating because we're using different objects, we could use test doubles or mocks or whatever to sort of simulate this and set things up and fake it but just as easily we could write regular old ruby classes instead. This is cool. If we change this code to always return the params hash, we notice that this should be a little bit different, and it fails because it tried to call to_date on nil, so it returned this and it was nil, so that means that our fake view to context class is working and while there's a little bit of set up here, it's definitely not the worst thing in the world. This is designed to point out, explain how to think about tests so that they're not just this magical thing. They really are just ruby code and they're calling the same code that you had before but you're cheating a little bit because this time you don't have rails available to you in the same manned that you had before.

Now we can take this and say: let's do date.yesterday and this should be date.yesterday and in theory if we run this on line 48, that should pass and it does, it did not fail. If we change this and let's make sure that it fails if we set up the wrong thing and it does. This is awesome because what we've done in just 10 lines of code or something there we've written a little class that we can replace and pass into our gem and it fakes the params hash and the views, does all this stuff and then really we call our code as we normally would and we can come in and pass in these dates and we don't have anything like Timecop or you change global ruby times or any of that stuff, we can purely do this all on our own without any gems, without any helpers, this is all that we've got. We've got regular old ruby code that genuinely tests our code and makes sure that it works. The only downside to this is that view_context has to accurately represent ruby's view_context, the code has to match up, otherwise if rails changes then this is going to have to change or it will break but all in all we can write some really really easy tests and be pretty confident that what we're doing works.

I'm actually going to cut it short here, because this is a lot of stuff to take in but testing is actually super easy, it's regular old ruby code, and you're just making sure that things equal each other. You don't have to worry about rspec or any of these extra gems, we don't have to worry about mocks or doubles or any of that stuff, regular old ruby code. That's what I want to get across. Hopefully that makes sense, and I will talk to you next episode. Peace v

Transcript written by Miguel

Discussion