[JAVA] What are the side effects? ~ Let's do a small unit test easily ~

What are the side effects?

Side effects in programming are when a function changes the (logical) state of a computer. Affects the results obtained after that. A typical example is the assignment of values to variables.

[wikipedia](https://ja.wikipedia.org/wiki/%E5%89%AF%E4%BD%9C%E7%94%A8_(%E3%83%97%E3%83%AD%E3% From 82% B0% E3% 83% A9% E3% 83% A0))

In other words?

It is a process that does not always return the same result even if it is called in the same way (roughly speaking).

What's wrong?

** Difficult to unit test! !! ** ** (Hereafter, in this article, automatic testing of source code using a testing framework like JUnit is called unit testing.)

It causes a lot of trouble

Show me a sample

I wanted to unit test this class, but it seems impossible.

Greet.java


@AllArgsConstructor
public class Greet {
    String first;
    String last;
    String gender;

    public void greet() {
        System.out.println(
                getFirstMessage() + forFullName() + getForGender()
        );
    }

    private String getFirstMessage() {
        int hour = LocalDateTime.now().getHour();
        if (6 <= hour && hour < 12) {
            return "Good morning";
        } else if (12 <= hour && hour < 18) {
            return "Hello";
        } else {
            return "Good night";
        }
    }

    private String getForGender() {
        if (gender.equals("M")) {
            return "Kun";
        } else {
            return "Chan";
        }
    }

    private String forFullName() {
        return first + " " + last;
    }
}

Main.java


public class Main {
    public static void main(String[] args) {
        Greet greet = new Greet("Yamada", "Takashi", "M");
        greet.greet();
    }
}

Where is it bad?

For these reasons, I feel this Greet is full of side effects

Give me examples of side effects

Processing that does not always return the same result even if called in the same way

For example, even if you execute it in the same way as greet.greet ();, the result will differ depending on the time of execution.

How will it be displayed? I don't know when this will be done ** when **!

Also, for example, if you do the following, the code on the 2nd and 4th lines is exactly the same, but the result is different.

Main.java


Greet greet = new Greet("Yamada", "Takashi", "M");
greet.greet();
greet.gender = 'F';
greet.greet();

How does that relate to not being able to test?

For example, if you write a test below in the morning, it's clear that it will fail at night.

greet.greet() == 'Hello Takashi Yamada'

Besides, greet () is void in the first place It doesn't even give me the opportunity to compare values because I'm happy with the standard output and it doesn't return anything.

(Of course, you can't do it exactly, but you need a hack for the current time and a hack for standard output.)

Then try to fix it

Of course I will fix it This code is unacceptable (although personally)

Take a look at Greet and organize what you are doing

Are you doing 5 things

  1. Change the first word over time
  2. Change the last word depending on gender
  3. Combine surname and first name with half-width characters
  4. Join them all in half-width
  5. Standard output

First and foremost, ** time ** and ** output **

When it comes to unit testing, the problem is how to implement 1 and 5.

You should see the campaign only on Saturdays, turn off the power at night, batch processing at the beginning of the end of the month, judge whether it is within the business hours of the partner company, etc. As a general rule, time should be passed from outside the decision logic (depending on the layer structure and component design). In this example, you can't test Greet without passing time from Main.

Then, if the assembled result is output as standard, it cannot be tested. So the result of the logic must be returned

Let's fix it

Greet.java


@AllArgsConstructor
public class Greet {
    String first;
    String last;
    String gender;
    LocalDateTime now; //Get it without generating it yourself

    public String greet() {
        return getFirstMessage() + forFullName() + getForGender(); //return
    }

    private String getFirstMessage() {
        int hour = now.getHour();
        if (6 <= hour && hour < 12) {
            return "Good morning";
        } else if (12 <= hour && hour < 18) {
            return "Hello";
        } else {
            return "Good night";
        }
    }

    private String getForGender() {
        if (gender.equals("M")) {
            return "Kun";
        } else {
            return "Chan";
        }
    }

    private String forFullName() {
        return first + " " + last;
    }
}

If you fix the above two, you will be able to write a test that says "** If it's X o'clock now, it should come back **".

GreetTest.groovy


class GreetTest extends Specification {
    def test_1() {
        setup:
        def greet = new Greet("Yamada", "Takashi", "M", LocalDateTime.of(2017, 7, 18, 12, 30, 00))

        expect:
        greet.greet() == 'Hello Takashi Yamada'
    }

    def test_2() {
        setup:
        def greet = new Greet("Yamada", "Takashi", "M", LocalDateTime.of(2017, 7, 18, 22, 30, 00))

        expect:
        greet.greet() == 'Good night Takashi Yamada'
    }
}

Field access

By the way, fix the point that the private method is accessing the field in the method This isn't always the case, and maybe it's object-oriented, but I usually write the code for the fixed example.

The benefit of fixing it is to clarify the relevant value and to eliminate the possibility that the state of the field has been updated in private. (I will show an example later, but I will make it a private static method and make field access impossible)

Greet.java


@AllArgsConstructor
public class Greet {
    String first;
    String last;
    String gender;
    LocalDateTime now;

    public String greet() {
        return getFirstMessage(now) + forFullName(first, last) + getForGender(gender);
    }

    private String getFirstMessage(LocalDateTime now) {      //Use only the value given as an argument
        int hour = now.getHour();
        if (6 <= hour && hour < 12) {
            return "Good morning";
        } else if (12 <= hour && hour < 18) {
            return "Hello";
        } else {
            return "Good night";
        }
    }

    private String getForGender(String gender) {             //Similarly
        if (gender.equals("M")) {
            return "Kun";
        } else {
            return "Chan";
        }
    }

    private String forFullName(String first, String last) {  //Similarly
        return first + " " + last;
    }
}

It's now easier to see what value each private method depends on only By the way, each private method depends only on the arguments and returns the result In other words, the side effects of the private method part are gone!

The fixed private method does not have any field access So it is possible to make it a static method

If static is attached, this cannot be used, so "I will set some flags while calculating" It is guaranteed that fields like

Given the meaning of the word static, the result does not change dynamically depending on the state, so can you somehow feel relieved?

(Static conversion is described together with the improvement below)

Staticization

Well, finally the finish Only greet () depends on the field, but you no longer need to store the value in the field separately.

Greet.java


public class Greet {

    //The field is gone

    public static String greet(String first, String last, String gender, LocalDateTime now) { //Here are all arguments
        return getFirstMessage(now) + forFullName(first, last) + getForGender(gender);
    }

    private static String getFirstMessage(LocalDateTime now) {
        int hour = now.getHour();
        if (6 <= hour && hour < 12) {
            return "Good morning";
        } else if (12 <= hour && hour < 18) {
            return "Hello";
        } else {
            return "Good night";
        }
    }

    private static String getForGender(String gender) {
        if (gender.equals("M")) {
            return "Kun";
        } else {
            return "Chan";
        }
    }

    private static String forFullName(String first, String last) {
        return first + " " + last;
    }

    //All methods are now static
}

However, you no longer need to do new, so you also need to change the caller.

GreetTest.groovy


class GreetTest extends Specification {
    def test_1() {
        expect:
        Greet.greet("Yamada", "Takashi", "M", LocalDateTime.of(2017, 7, 18, 12, 30, 00)) == 'こんにちは Yamada Takashi くん'
    }

    def test_2() {
        expect:
        Greet.greet("Yamada", "Takashi", "M", LocalDateTime.of(2017, 7, 18, 22, 30, 00)) == 'おやすみ Yamada Takashi くん'
    }
}

Now this test will pass at any time, and I can't just call it with this argument to get another result!

It's against object orientation! ??

Certainly, I feel like that ... In the first place, what I find attractive about eliminating side effects is the influence of functional languages.

However, that's not even an impression, so I thought about it for a moment.

Are there three types of classes when writing object-oriented code?

Yeah, this is the text I just came up with

  1. A class that does not have a value and judges or calculates based on the received value
  2. A class that has a value and behaves based on its own value
  3. Classes that handle them

Let's look at each one

Needless to say, the top class is the Greet class that I fixed earlier. It doesn't have its own value, it only depends on its arguments

What is the second class I think this is the most object-oriented class, but it is a class that expresses "things" such as "personal name" and "gender".

Let's make it appear in the previous topic.

First.java


@AllArgsConstructor
public class First {
    @Getter
    private final String value;
}

Last.java


@AllArgsConstructor
public class Last {
    @Getter
    private final String value;
}

Gender.java


public enum Gender {
    M, F
}

User.java


@AllArgsConstructor
public class User {
    private final First first;
    private final Last last;
    private final Gender gender;

    public String asFullNameString() {
        return first.getValue() + " " + last.getValue();
    }

    public boolean isM() {
        return gender == Gender.M;
    }
}

I made a class that represents "people" called ʻUser`, and tried moving gender determination and full name concatenation there.

I feel that this is actually a pretty good division of responsibilities. The reason is that it turned out that the parts such as Greet" Gender is compared with M" and "Last name and first name are connected by half-width" are not actually related to greetings. This is because all that is required for greetings is "to process the first and last name a little according to time and gender", and "male is M "or" half-width connection "is a process that conforms to the" definition of a person ". (Of course, it may not be the case depending on the specifications and design)

And of course ʻUser` unit tests, this is absolute

If you move "male judgment" and "first and last name concatenation" to ʻUser and unit test ʻUser, the test of Greet will only be time zone judgment and Kun-chan

Greet.java


public class Greet {
    public static String greet(User user, LocalDateTime now) {
        return getFirstMessage(now) + user.asFullNameString() + getForGender(user);
    }

    private static String getFirstMessage(LocalDateTime now) {
        int hour = now.getHour();
        if (6 <= hour && hour < 12) {
            return "Good morning";
        } else if (12 <= hour && hour < 18) {
            return "Hello";
        } else {
            return "Good night";
        }
    }

    private static String getForGender(User user) {
        if (user.isM()) {
            return "Kun";
        } else {
            return "Chan";
        }
    }
}

Greet is refreshing again

  1. Classes that handle them

And last but not least, this time it corresponds to Main.

ʻUseris created and calculated usingGreet` (code is omitted)

Summary of 3 types

In other words, it might look like this

  1. Logic
  2. Things
  3. Processing

The logic is basically static and should not be state dependent

Things hold values and behave based on them, but hide the values themselves and the logic itself (Is it equivalent to encapsulation?)

There is a treatment layer that allows the two to work together, side effects are only allowed in the treatment layer (Since side effects cannot be eliminated, I will do it here. This class test will be done using a mock etc., but it will be done again soon)

Digression? About the boundary between logic and things

Is it the decision of Greet to say" if you're a man, if you're a woman "? ʻUser` Isn't it?

"ʻUser` is combined with half-width space, but when sending an email, it must be combined with full-width".

You might think I thought for a moment

When I thought "logic?" Or "thing?", I made "logic-only things", but recently I've calmed down.

In other words, create "ʻUser for Greet` "and write" Kun-chan "and" half-width combination "in the greeting-specialized user class. If it is different for email, we will set up a separate email-specific user class.

I think this is a further division of responsibilities, or a clearer boundary. If you do this, the number of classes will increase, but I feel that the number of test items will not increase so much, and above all, the dependence will decrease.

You don't have to do something like "re-evaluate greetings due to changes in email specifications"!

Summary

Examples of side effects

Be careful if you see this area! !!

When referencing, do it outside of "logic, thing", and "logic, thing" must not depend on it Do not write the assembled value on the spot, return it to the processing layer once, and write it to the processing layer again

Easy judgment guide

To put it simply, side effects are mixed if the following is required in the "logic, thing" test.

If you do these things thoroughly, you will reduce the code that is difficult to evaluate, increase the number of unit tests, and improve the pace of development.

See you again

Recommended Posts

What are the side effects? ~ Let's do a small unit test easily ~
What to do if you get a "302" error in your controller unit test code in Rails
Let's cure the unit test like breathing (Apex trigger edition)
What to do when the changes in the Servlet are not reflected