The first year of new graduates thought about an implementation method that makes use of TDD (ruby)

Why the first year of new graduates focused on TDD

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

Since I am inexperienced, I have no habit in the mounting procedure. Because I don't have a habit, I decided to try to implement it using TDD in order to get a good habit (maybe there was a different way).

What I actually did

What is TDD

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

Image of TDD

Write a test (before implementation) ↓ Test fails (because I haven't implemented anything) ↓ Implement ↓ Tests succeed (because the implementation allows you to pass the tests you wrote before implementation) ↓ Perform refactoring. If the test is successful, then we can say that we were able to fix the code without losing functionality. On the contrary, if the test fails due to refactoring, the function that should have been implemented has been lost, so debug it.

Advantages and disadvantages of TDD

Benefits of TDD

Disadvantages of TDD

Indicator of TDD

In order to suppress the disadvantages of TDD and take advantage of it, it is necessary to have an index of when to TDD.

In short, it's okay to try basic TDD, but if you don't know how to write a test first, you shouldn't do TDD.

Specific example of test when doing TDD

Here is a concrete example of a TDD test. The explanation uses various test types (model, controller, integration, etc.), so if you don't know about the test types, please take a look here. Rails test directory structure and test process

Simple test

Success when requested or verification of the existence of required elements (simple controller test). It's a good idea to write a simple confirmation test first

controller_test.rb


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

Validation

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

user_test.rb


#User model testing
#Write the following test before describing the validation content in the user model
  def setup
    @user = User.new(name: "Example User", email: "[email protected]m",
                     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

users_signup_test.rb


#New registration test(integration_test)I do
#Tests that you want to fail when trying to register with parameters that are invalid
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 feature modification (debugging)

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

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

Login_controller.rb


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

  private #From now on, the definition of the function used in the action

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

  #Log out the current user
  #Current indicating the current user_Empty the user's memory token and session,Variable current_Empty the contents of user
  def log_out
    forget(current_user) 
    session.delete(:user_id)
    current_user = nil
  end
end

In the state of this code, 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, current_user is nil, so it becomes nil.forget and an error occurs.

In such a situation, it can be said that "there is no particular error statement, but it is not working as I expected", so I do TDD. Write a test first to see if it works properly when you log out twice.

users_login_test.rb


def setup #Define a user instance to log in to
    @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 logging out in another tab
    delete logout_path
    follow_redirect! #When redirected, check if the page is the one before login
    assert_select "a[href=?]", login_path #Check if there is a path for the login page
    assert_select "a[href=?]", logout_path,      count: 0
    assert_select "a[href=?]", user_path(@user), count: 0
end

Make functional changes to pass this test. Finally, add the following code and the test will pass.

Login_controller.rb


class LoginController < ApplicationController
.
.
  
  def destroy #Action to log out
    log_out if logged_in? #Use logout function only when logged in
    redirect_to root_url
  end
end

Tips / Others

[Test Driven Development](https://www.amazon.co.jp/%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9 I learned the tips to use when implementing TDD from the book% 96% 8B% E7% 99% BA-Kent-Beck / dp / 4274217884).

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

This time, I will give an example of TDD for creating a function that returns the input argument (int) as a character string.

Minimum test creation

Create a test for a function (not yet created). At the time of creation, the test will be red.

  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 written above in any form. The test turns green in the function definition.

  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 Rera test passes.

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 from a 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 you do TDD for the minimum implementation as described above, the code will become complicated as more functions you want to add. but it's okay. Thanks to writing the test, you can judge the function of the code accurately. Therefore, you can refactor with confidence.

What I felt when I practiced TDD

users_signup_test.rb


#Test new registration
#Tests that you want to fail when trying to register with parameters that are invalid
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

#Tests that you want to succeed when 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

controller


  def create
    if (user_id = session[:user_id])
      .
      .
    else
      raise       #If the test passes, you know that this part has not been tested → Write the 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

Recommended Posts

The first year of new graduates thought about an implementation method that makes use of TDD (ruby)
[Ruby] Questions and verification about the number of method arguments
About the behavior of ruby Hash # ==
About the role of the initialize method