RailsConf 2015
Understanding Rails Test Types in RSpec

Understanding Rails Test Types in RSpec

by Sam Phippen

In this video titled 'Understanding Rails Test Types in RSpec', speaker Sam Phippen discusses the challenges faced by developers when starting to test Rails applications, particularly focusing on automated testing methodologies using RSpec. The presentation aims to demystify the various test types available in the Rails ecosystem and provide guidance on structuring tests effectively to enhance code reliability.

Key Points Discussed:
- Importance of Automated Tests: Phippen emphasizes that while comments can often misguide developers, tests provide an objective truth about the system. Good tests serve not just as code, but as a form of documentation that communicates expected behavior.
- Common Test Types: The talk covers three main categories of tests in Rails applications:
- Model Tests: These tests focus on the core logic of the application, verifying that models behave as expected.
- Controller Tests: These tests ensure that the controllers function correctly by testing the HTTP responses and behaviors.
- Feature Tests: These tests simulate user interactions to validate that the application functions as intended from the user's perspective.
- Testing Approach: Phippen introduces the 'outside-in' testing method, where tests are first written from a user-centric perspective before drilling down into individual components like controllers and models. This approach aids both in organizing tests and understanding the application better.
- Live Coding Demonstration: During the talk, Phippen conducts a live coding session using a simple to-do list Rails application. He demonstrates how to implement tests step-by-step, starting from writing a failing feature test to progressively incorporating functionality that results in passing tests. He highlights the importance of writing expectations in clear, user-centric language to improve readability.
- Refactoring and Test-Driven Development: The presentation illustrates how iterative development through testing contributes to cleaner, more manageable code. Phippen highlights how model tests can be utilized to confirm behavior directly related to the business logic of the application.

Conclusions and Takeaways:
- Testing is a critical part of developing robust Rails applications, and understanding different test types is fundamental for beginners.
- Writing effective tests not only aids in catching bugs but enhances the overall communication and understanding of the application’s expected behaviors within the team.
- The 'outside-in' testing approach is beneficial for structuring tests and ensuring a solid framework for handling changes in code as the application evolves.

00:00:12.759 Hello, everyone. My name is Sam Phippen. You can find me on Twitter as @SamPhippen and on GitHub also as Sam Phippen. If you're interested in learning more about me, feel free to take a look at my profiles. I spend most of my time on GitHub, where I am a member of the RSpec core team. My presence here is because I believe it's important for RSpec to be well represented at community events like this. Everyone who uses the framework should have the opportunity to talk to someone who works on it regularly. If you see me at the conference, please don't hesitate to come and chat. I will be running office hours at the Heroku Community booth on Wednesday during the happy hour.
00:00:44.760 I work for a company called Fun and Plausible Solutions. We are a consulting agency specializing in data science and refactoring tests for large, complicated Rails applications. If you're interested in working together on projects like this, please do come and chat with me afterwards. I always start my talks with the statement that I believe discussions are more interesting than sticking strictly to a pre-planned agenda. Therefore, I encourage you to ask questions as I'm speaking. If you're not following something or if I'm explaining poorly, your questions can help clarify my thoughts and assist everyone else in understanding as well.
00:01:08.479 If you have a question, it’s likely that someone else in the room shares your confusion. So, I want to start this talk by discussing one of the many motivations for writing automated tests for our applications. Many beginner developers are taught that testing is essential, but the reasons often provided are flimsy or easily debatable. Here’s one idea I’d like to share: comments can sometimes mislead. It’s not that when you write comments in your code, you intend to misguide your colleagues; rather, you're trying to help them understand the software you've built.
00:02:09.400 However, during high-pressure situations—like tight deadlines or rapid changes to the code—it can be easy to neglect comment maintenance. As our application's code evolves, the meaning behind it may also change, leaving outdated comments that no longer reflect the reality of the code. When you're trying to decipher complex code, having misleading or poorly written comments can create erroneous assumptions that complicate your understanding. In contrast, tests do not lie. They provide an objective truth about the system they test. Tests are essentially code, and when you run them, they interact with the system, yielding clear insights about its functionality.
00:03:10.519 That said, it's still possible to write bad tests that could be as confusing as ambiguous comments. However, unlike comments, good tests enable you to aggregate knowledge about your application into an executable format, allowing you to communicate effectively with your team through actual test code. Let’s transition to discussing Rails. There were significant announcements regarding changes in Rails 5 that will happen this morning. Unfortunately, I haven't had the opportunity to update everything I planned to discuss regarding the new Rails 5 changes. Nevertheless, I maintain that Rails often raises the barrier to entry for testing more than it should, which may hinder your ability to write your applications.
00:04:11.440 One reason beginners often struggle with testing in Rails is that Rails offers various test types that serve different purposes, making it hard to grasp their collective function. Today, I’ll cover the three most common test types encountered in Rails applications: model tests, controller tests, and feature tests. These groups of tests help encapsulate the majority of your application's behavior as you begin developing it. In my experience, I typically reach for one of these three test types before opting for others.
00:05:06.959 We will also discuss a testing approach called 'outside-in' testing, which is a preferred pattern for structuring your tests. This method not only aids in organizing the tests you write but also enhances your understanding of the code you will be testing. The concept behind outside-in testing is to start by writing tests that describe your application from a high level, focusing on the user experience as a browser or HTTP client would. Your application is treated as a black box; you do not look inside it, and you don’t delve into the database or any internal components. You test your Rails app as a user would experience it, validating expected behaviors.
00:06:08.640 Once you've established satisfactory outside tests, the approach encourages you to write tests inside your application—focusing on individual controllers and models. This two-layered testing structure, one from a user-centric perspective and another from a code-centric view, provides rigorous verification of expected behavior, facilitating clear communication about your application’s functionality. To clarify the theory behind outside-in testing, I plan to conduct about 20 minutes of live coding. If everything goes smoothly, we should end up with a solid understanding of the Rails test types. Afterwards, I will open the floor for questions.
00:07:10.680 Now, let’s switch over to my terminal. I’m going to demonstrate a simple Rails application we’ll be testing today. This application is a basic to-do list manager with one model and one controller. The to-do items are uncomplicated, featuring a done flag that indicates completion and a text note to describe each task. I can set a to-do item as done using the Rails admin console, and you will see that change reflected in the application.
00:08:18.520 Let’s consider a situation where our customer requests that we separate the to-do items into two distinct lists: one for completed items and another for incomplete ones. We’ve decided to implement this change in an extremely test-driven manner. If I run my tests now, I see that there’s a single failing feature test stating that it expected to find a header for completed to-dos.
00:08:41.400 Let's dive into the test file and understand what this failing test indicates. This is a Rails feature test, primarily utilized for the outside portion of the outside-in testing cycle I mentioned earlier. This test solely interacts with the web browser, expecting specific results to be visible to the user. It does not concern itself with the model or controller layers or any underlying components of the application.
00:09:52.479 To create an RSpec feature test, you declare it with the type feature. This informs RSpec to include specific methods in your test that enable interaction with the simulated browser used to drive Rails applications. The structure of the rest of this test is particularly interesting as it does not simply describe the actions in traditional RSpec terms but instead uses two methods named visit_homepage and should_see_completed_todos_header. These methods directly invoke functionality from RSpec’s testing framework, enhancing the readability and understandability of the test.
00:10:46.920 The reason I’ve structured the test this way is that I want it to resemble user language. For instance, it describes visiting the homepage and checking for the completed to-dos header. These methods are defined in a module called TodoSteps. By visiting the homepage, we expect to see the content 'Completed Todos'. If I modify the index.html.erb page to include a header stating 'Completed Todos', the test should pass. Running the test confirms it does pass. Taking a glance at the browsing application, we can see that the header has been correctly added.
00:12:12.320 While this might seem like a trivial change, it’s important because it establishes a foundational test that will allow us to add further tests to the application. Next, we will write a test that creates a completed to-do item and expects it to appear on the proper list. To do this, I will add a context block to describe the scenario with a completed to-do, which will add the necessary setup for our test.
00:12:52.480 As we create the completed to-do in the test context block before each test, this ensures the to-do gets created under the conditions we set in the test. The expected behavior here is that the completed to-do will appear in the completed to-dos list on the homepage. I will also need to visit the homepage again to verify that table displays correctly and assert that I see the completed to-do with a note 'by milk'. Now, when I execute this test, it should fail, indicating it cannot find the corresponding CSS ID for completed to-dos.
00:14:43.760 The expectation in the test checks for an HTML element with the ID 'completed_todos' and verifies the content within it. To make this test pass, I will take the existing table code from above, paste it into the proper place for completed todos, and set the ID correctly. Upon running the test again, it should now pass. Let's analyze the to-dos spec we’ve written so far: we’ve added a header and created a separate table we built to list our completed todos, but we still might not be finished.
00:15:17.520 This scenario only covers one completed to-do, while the behavior for incomplete to-dos still needs to be addressed. At this point, I will introduce a new context for incomplete to-dos. We'll handle this similar to how we structured prior tests. I will make a copy of the setup for the existing tests, updating it to reflect the changes for an incomplete to-do, specifying its note as 'by eggs'. The essential feature we expect is the completed to-do appears on the completed table, while the incomplete to-do should not.
00:16:17.120 I will iterate through the basic test setup, and for the new test, I will assert that after visiting the homepage, I do not find an incomplete to-do listed with the note of 'by eggs'. The running test should initially fail. Now, I need to finalize any necessary adjustments to the controller so that it properly reflects the conditions of the test. The 'to-dos' instance variable will be used for our completed items in that table, so I will make sure that it only contains items marked as done.
00:17:32.480 This changes the implementation within the Todo controller to filter for completed items accurately. When I define completed_todos as 'ToDo.where(done: true)', the running tests will pass. The next immediate task is to ensure the related feature tests now validate the correct data flow and display functionalities.
00:18:59.600 Let’s look at how the live coding approach implemented a testing-driven workflow for the Rails application. By examining how the feature tests pass, we verify expectational behaviors, leading to successful implementations. Having established completed todos now listing correctly, we’ll move to focus on the next area of refactoring the to-do controller.
00:20:19.760 The goal behind the index action currently involves too many abstraction levels with what it executes. The line that assigns 'todos' directly from the database convolutes the behavior; meanwhile, defining 'completed_todos' as 'ToDo.where(done: true)' is a more procedural setup that should live within the model. To help support future maintenance, it is sensible to push that logic down to the ToDo model.
00:21:44.160 Therefore, I will write a proper model test, it will induce of creating completed and incomplete ToDos directly in our test so that we can confirm that our completed method accurately retrieves only completed items upon call. Once I compose it, the failing test will help guide the creation of the necessary method within the ToDo model.
00:23:30.680 With each iteration across the test development, we see the nature of test-driven development enhancing our code quality through cycles of verification. Returning to our controller allows us to drop in a method that accurately defines our expected behaviors within the contexts of the application.
00:24:48.440 As we look at our tests totaling over these mini cycles of development and interaction, we can clearly see that we’ve moved to a more manageable implementation of separated completed to-dos. At this juncture, I wanted to share some closing remarks.
00:25:51.680 First, it’s important to grasp that while it may seem like a natural result of engineering, testing Rails applications with RSpec — a lot of effort has gone into making this possible. Rails is a shifting platform, with new updates and features evolving over time. The community owes thanks to individuals like Aaron and Andy, who have significantly contributed to these tools.
00:27:01.080 I traveled from London, and while the flight is bearable, I always appreciate my experiences in the US, including the culinary treats, though I must say—sweet tea is an intriguing challenge to me.
00:27:31.480 I want to thank you all for your attention, and if there are any questions, I’ll be happy to answer them.