[Ruby] The first year of new graduates considered the implementation method using TDD (ruby)

8 minute read

Why did the first year of new graduates pay attention to TDD?

Currently working as an engineer in the first year of new graduates. I had little engineering experience and was wondering if I could use my inexperience to beat my seniors.

Since I have little experience, I have no habit in the implementation procedure. I didn’t have any habits, so I thought I’d try to make good habits and try to implement TDD-based implementation (maybe there was a different way).

What I actually did

What is #TDD Test-driven development. Before implementing it, write a test that only passes if the implementation is successful.

TDD image

Write test (before implementation) ↓ The test fails (because I haven’t implemented anything) ↓ Implement ↓ The test succeeds (since the implementation allows the tests I wrote before implementation to pass) ↓ Refactor. If the test was successful, we were able to fix the code without losing functionality. On the other hand, if the test fails due to refactoring, you have lost the functionality that you should have implemented, so debug it.

Merits and demerits of TDD

Advantages of TDD

  • Safe refactoring
  • Prevent bugs by adding features

Disadvantages of TDD

  • Writing each time is a hassle
  • I don’t know what test to write
TDD indicator

In order to suppress the disadvantages of TDD and make the most of them, it is necessary to have an index of when to use TDD.

  • Simple test (eg whether request is successful) → TDD
  • The stage when the operation and development contents are not completely determined → Development is first
  • When you want to confirm development and validation that are important for security → TDD
  • I want to refactor → Write a test first and check if the test passes even if refactored

In short, the basic TDD is good, but if you don’t know how to write a test, you should not do TDD.

Example of test when doing #TDD

Here is a specific example of a TDD test. This is an explanation using various test types (model, controller, integration, etc.), so if you do not know the test types, please see here. rails test directory structure and test process

Simple test

Whether it succeeds when requested or the existence of necessary elements is confirmed (simple controller test). It is good to write a simple confirmation test first

controller_test.rb


# Controller test
  test "should get home" do
    get root_path
    assert_response :success is # Is the request successful?
    assert_select "title", "HOME TITLE" # Is the title of the page HOME TITLE?
  end

Validation

First write a test that succeeds if validation is successful. (Example: integration test to check the result of model test, form submission)

  • When checking validation for model class

user_test.rb


# User model testing
# Write the following test before describing validation contents in user model
  def setup
    @user = User.new(name: "Example User", email: "[email protected]",
                     password: "hogehoge", password_confirmation: "hogehoge")
  end
  
  test "should be valid" do
    assert @user.valid?
  end
  
  test "name should be present" do
    @user.name = ""
    assert_not @user.valid?
  end
  
  test "email should be present" do
    @user.email = ""
    assert_not @user.valid?
  end
  • When confirming the validation for transmission using form (new registration, login, etc.)

users_signup_test.rb


# Test new registration (integration_test)
# Test that wants to fail when trying to register with invalid parameters
test "invalid signup information" do
    get signup_path
    assert_no_difference'User.count' do
      post users_path, params: {user: {name: "",
                                         email: "[email protected]",
                                         password: "foo",
                                         password_confirmation: "bar" }}
    end
end

TDD for function modification (debugging)

There is no particular error statement, but I wrote a test first when it is not working as I expected. Write a test that only passes when it works the way you want it, and then modify the code to make it work the way you want.

The logout process will be described as an example. I referred to Ruby on Rails Tutorial 9.14 (Two inconspicuous bugs).

Login_controller.rb


class LoginController <ApplicationController
.
.
  
  def destroy # action to log out
    log_out
    redirect_to root_url
  end

  private # After this, the definition of the function used in the action

  # Destroy a persistent session
  def forget(user)
    user.forget # Empty user's login remember token stored in DB
    cookies.delete(:user_id) #empty the contents of cookies
    cookies.delete(:remember_token)
  end

  # Log out current user
  # Empty the memory token of current_user that indicates the current user, and empty the contents of session and variable current_user
  def log_out
    forget(current_user)
    session.delete(:user_id)
    current_user = nil
  end
end

With this code state, Open two tabs in your browser and in each tab Suppose you are logged in as the same user.

First, log out in one tab. Then the value of current_user becomes nil. After that, when I try to log out on the other tab, the current_user is nil and it becomes nil.forget, which results in an error.

Such a situation can be said to be “when there is no particular error statement, but it is not working as expected,” so TDD is performed. I write a test to check if the operation is normal when I log out twice.

users_login_test.rb


def setup #login define user instance
    @user = User.new(name: "Example User", email: "[email protected]",
                     password: "hogehoge", password_confirmation: "hogehoge")
end

test "two times logout after login" do
    log_in(@user)
    delete logout_path
    assert_not is_logged_in?
    assert_redirected_to root_url
# Simulate that you logged out in another tab
    delete logout_path
    follow_redirect! #If redirected, check if the page is the one before login
    assert_select "a[href=?]", login_path #login Check if there is a path for the page
    assert_select "a[href=?]", logout_path, count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
end

Make changes to the functionality to pass this test. Finally adding the following code will pass the test.

Login_controller.rb


class LoginController <ApplicationController
.
.
  
  def destroy # action to log out
    log_out if logged_in? Use the logout function only in the #login stateredirect_to root_url
  end
end

Tips/Others

Test Driven Development.

In this book, it was written that at least test creation → provisional implementation → triangulation → refactoring should be done.

This time, the TDD for creating a function that returns the input argument (int) as a character string is taken as an example.

minimum test creation

Create a test for the function (not yet created). The test is red when created.

  test "Pattern1 of test returnString function" do
    a = returnString(1)
    assert_equal a, "1"
  end

Temporary implementation

Define a function that will pass the test described above in any way. The function definition makes the test green.

  def returnString(int)
    return "1"
  end

Triangulation

Create multiple tests of the same type (two this time), and rewrite the implementation to a general form so that the test of the Lera passes.

  • First add another similar test

When I create this test it becomes red. Because the current implementation only returns “1”.

  test "Pattern2 of test returnString function" do
    b = returnString(2)
    assert_equal b, "2"
  end
  • Change the implementation to a general form

Change the function that returns a raw value of 1 to a function that returns the value received as an argument as a string. The test will be green.

  def returnString(int)
    return "#{int}"
  end

Refactoring

Even if TDD is performed for the minimum implementation as described above, the code will become more complicated as the number of functions to be added increases. but it’s okay. Thanks to writing the tests, you can determine the exact functionality of the code. Therefore, you can refactor with confidence.

What I felt when I practiced #TDD

  • You can think of what you want to implement by doing TDD. I felt it made the implementation efficient. Also, I felt that it would be easier to maintain the maintainability of the code because the refactoring can be done with confidence by writing the test.

  • Not limited to TDD, when you write a test, you do not have to try to write multiple types of test contents at once. If you try to write at once and the content of the test itself is wrong, it will fall down

  • It is not a good idea to over-write the contents of the test, even if it covers the contents above. If you write too comprehensively (check thoroughly the existence of HTML elements, etc.), it will increase the effort to maintain and modify the test itself if there is a change in the function in the future.

  • Not limited to TDD, when writing a validation test, you must write both the test when you want it to work and the test when you do not want it to work.

users_signup_test.rb


# Test new registration
# Test that wants to fail when trying to register with invalid parameters
test "invalid signup information" do
    get signup_path
    assert_no_difference'User.count' do
      post users_path, params: {user: {name: "",
                                         email: "[email protected]",
                                         password: "foo",
                                         password_confirmation: "bar" }}
    end
end

# Test that you want to succeed by trying to register with valid parameters
test "valid signup information" do
    get signup_path
    assert_difference'User.count', 1 do
      post users_path, params: {user: {name: "Example User",
                                         email: "[email protected]",
                                         password: "password",
                                         password_confirmation: "password" }}
    end
end

By writing * raise, an error is intentionally generated in that situation. If you write raise but all the tests pass, it means that the part that describes raise has not been tested. Write raise in the conditional branch (if statement) and check whether the branch is tested. Write a raise on the suspicious part to see if it is being tested.

controller


  def create
    if (user_id = session[:user_id])
      .
      .
    else
      If the raise # test passes, you know that this part has not been tested → write a test so that this part is tested
      .
      .
    end
  end

References

Ruby on Rails Tutorial [Test Driven Development](https://www.amazon.co.jp/%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9(%96%8B%E7%99%BA-Kent-Beck/dp/4274217884) Clean code that works-How can we go there?-Takuto Wada | SeleniumConf Tokyo