RSpec
RSpec tests, RSpec specs, and RSpec examples mean the same thing throughout this book.
In terms of scope, the RSpec examples in Anki Books are unit tests, component tests, and small integration tests. This terminology around classifying tests has different meaning to different people. Unit tests test the smallest units of the system in isolation and the smallest units are typically functions or methods that do not need to call other methods. Component tests test the combinations of units and units working together. Integration tests are then one level greater in scope than component tests. Remember this terminology is not universally agreed upon. You might call tests that show the system working correctly acceptance tests. You might call tests that interact with the application as a user agent like a web browser would feature tests. You might also say automated tests, tests that automate a web browser, and end to end tests. With Rails in particular, tests are often classified by what class hierarchy the class being tested belongs to. For example, there are model tests and controller tests (now request specs are preferred). There are also view specs, routing specs, etc.
Testing something in isolation can involve mocking the things that it interacts with. For example, if you have an object that sends a message to a different object and gets a value in return, that other object could be replaced by a double and the return value can be stubbed. The implementation of the real object's method might have its own unit tests elsewhere. This can be a tricky thing in practice as it is possible to change the API of something and break the production class while tests continue to pass because they rely on the test double which has not been updated. Dependency injection, the practice of injecting a dependency into an object rather than that object constructing the dependency itself, can make testing easier with or without using a double for the dependency.
An RSpec example is a test or spec that is defined using the RSpec it method. An RSpec example group is a group of examples defined using either one of the RSpec context or describe methods, which are aliases. To the machine, context and describe are the same method, but conventionally they are used in different ways. Keep in mind that the compiler is not the only audience for source code. Following conventions will help people in the future (including you) understand the intentions of the code. Using describe and context in the conventional way makes the design of the test code more explicit and following established conventions which are good characteristics.
The RSpec examples in Anki Books are divided into groups by directory structure. There are model specs, request specs (not controller specs), routing specs, and other types. You will see the model specs in spec/models, view specs in spec/views, etc. Anki Books also uses the convention of splitting the test code over the largest number of files that it would make sense to. The majority of test code files contain tests of only one method each.
Tests demonstrate correctness and provide a safety harness
Test code is not only used to test the correctness of the program for valid inputs and correct failure behavior of the code for invalid inputs at first write. Test code also serves as a safety harness, preventing regressions into the future at a cost of maintaining the test code. Keep in mind it is no possible to completely prove that there are no bugs by writing tests. You can prove that there are bugs though.
Test code can be written for additional purposes at the same time as it is used to show correctness of the program.
Tests provide executable documentation
Specs is short for specifications. That we say specs instead of tests when talking about RSpec emphasizes that these examples are not only for demonstrating some degree of correctness. RSpec test code provides reliable documentation of what the program does. When writing the string argument to the RSpec it and context methods, you should occasionally run the rspec command with the --format doc long option to see the documentation output, and do your best to make it a clear documentation.
Test-driven development is an important skill to absorb
Test-driven development is an approach where you write your tests before or as you write the code. It may have some disadvantages in that it might lock you into a suboptimal design too early, but most of the time it will help you design better.
Writing tests as you write code guides you toward a better design of that code. In order to test code, you will need to use the API of your code. If using the API of your code in tests is really hard, how is it going to be easy to reuse by other parts of the application? If you do not write any test code for your code, your code has no reason to be easy to test and starts off with no tests. Software without tests is bound to fail, so you can expect it to keep failing and failing until eventually you find yourself writing the tests anyway. Unfortunately, by that point, the code is not easy to test because it was not designed to be and you're less familiar with it.
Example: routing specs
Routing specs are super easy to write using test-driven development: write the tests first and watch them fail, then write the code and watch them pass. Test-driven development is a patient practice that will result in software with tests and APIs that are easier to use by the higher-level layers in the code lasagna (see Code Craft for code architecture as pastas).
RSpec.describe HomepageController do it "routes to #homepage" do expect(get: "/").to route_to("homepage#show") end it "routes to #study_cards" do expect(get: "/study_cards").to route_to("homepage#study_cards") end end
The code above defines the routing of the root path of the website to a Ruby class called the HomepageController, a class derived from ApplicationController which all Rails controller classes extend. The controller is a central player in the Model-View-Controller pattern given to us for free by Ruby on Rails. Controllers in Rails almost always have a name with a plural word before controller like ArticlesController. ApplicationController is an exception and here I chose to have its child class HomepageController be similarly an exception. The public methods of controllers in Rails that are matched to paths are called actions. Here we have two routing specs that are asserting true the path "/" is matched to the show action and the path "/study_cards" is matched to the study_cards action of the HomepagesController. Remember one of the two driving principles of Rails: Convention over Configuration--here you will see that the naming of the different types of things is following these conventions that removes some of the need for boilerplate code, which is necessary code, but it slows us down.
The following is all that is needed to make the above routing specs pass (this code is from the routes.rb file, which uses a domain specific language to specify the routes):
root "homepage#show" # Dependency: paths that render study_cards/index must end with /study_cards get "/study_cards", to: "homepage#study_cards", as: "homepage_study_cards"
A lot of programmers would see a comment like that and say, hey no comments are needed, your code is completely readable on its own. It violates DRY to have an obvious comment above code which is clearly readable. You don't need a comment like "Find the book by id" above the code Book.find params[:id]. The comment above is clarifying a dependency. Dependencies are super important things to notice in understanding the spaghetti. A great book on Ruby, Practical Object-Oriented Design, argues the main idea in practical design is preserving changeability by minimizing dependencies. It's best to not have any dependencies that are unnecessary, but here the dependency is a tradeoff for a benefit elsewhere. If it is not possible to improve the code anymore (we have other things to do too), it could be a good place to use a comment. By clarifying a dependency with a comment, we are decreasing complexity by reducing how likely it is for things to break unexpectedly, which would happen eventually with that kind of special convention. A comment can be used to reduce complexity of the code in this way: explaining why, not what the code is doing (remember DRY).