In our previous article , Many of the advantages of functional programming have been repeatedly emphasized , For example, the ability to express , The benefits of deferred Computing . But a bigger one is actually testability . This article is also the core of the whole series , We don't want to completely eliminate the process 、 Side effects, etc , But limited use , And can make improvements on the basis of existing code .
below , Let's look at an example ： A company wants to design a time-based scheduler , They can provide a better than
crontab Better grammar , For example, it can be based on the first three days of each month 、 Weekly weekend 、 The first day of the second week of each month . When designing this scheduler , It will involve many functions of time , such as , Here is a possible function to implement ：
from datetime import datetime, timedelta def yesterday_str() -> str: """ Get the string of yesterday's time (YYYYMMDD) """ return ( datetime.now() - timedelta(days=1) ).strftime("%Y%m%d")
This is the most intuitive implementation , however , This function is found to be unmeasurable . You should see the reason , Because of
datetime.now() It has side effects . Specifically, we can give examples of possible problems in the test as follows ：
Problems in unit test examples
How do we write the unit test of this function , Obviously , Most people would write that ：
def test_yesterday_str(): assert yesterday_str() == ( datetime.now() - timedelta(days=1) ).strftime("%Y%m%d")
Obviously , This unit test reveals several problems at a glance ：
- in fact , We just rewrote the original code , There is no real test .
- Even if we admit this way of writing , There is also a certain probability that near the early morning （23:59:59 seconds ）, The test failed , But this is not an error caused by the problem of function implementation .
Problems in integration testing
In the actual test , Maybe some parts of the integration are more difficult to test , For example, let's the next function that calls the above function , Its function is every month 1 No. 1 performs a task ：
def run_at_first_day(): if yesterday_str()[-2:] == '01': do_something()
This example not only passed on the side effects step by step , And in the test , If we weren't 1 Test No , We can only measure
do_something Not measured by logic
run_at_first_day The logic of this scheduling . And you can imagine , In this system , There will be many such examples .
How to solve
Conventional solutions , The first is to modify the system time .
Python There is one of them.
FreezeGun Module , Is to do something similar ：
from freezegun import freeze_time import datetime @freeze_time("2012-01-14") def test(): assert datetime.datetime.now() == datetime.datetime(2012, 1, 14)
Of course , The solution is for Time It's all right , We may have more than one side effect , It may be reading the configuration 、 Database interaction and so on , This solution can't solve these things .
The other is the concept of testing field , such as
stub Such concepts , Of course, we will also use it in the following work
fake The concept of , But there is no need to dwell on these complex concepts .
Take the side effect function as a parameter
Let's rewrite it in the following way , You find that the whole function becomes measurable ：
def yesterday_str(now_func = date.now) -> str: assert yesterday_str() == ( now_func() - timedelta(days=1) ).strftime("%Y%m%d")
The specific test method is as follows ：
def fake_now(now_str): def helper(): return datetime.strptime(now_str, "%Y-%m-%d") return helper def test_yesterday_str(): return yesterday_str(fake_now('2020-01-01')) == '2019-12-31'
We find that there are many advantages to writing like this ：
- The whole function becomes no side effects , Side effects are isolated in the parameters
- Because there are no side effects , We just need to make the corresponding 「 false 」 Function can simulate the input to be , Especially for
Void -> AThis type of function .
- We can simulate the operation of any state by means of pseudo function , This makes the scheduling logic we mentioned above testable .
- When we specifically call , Because the default value of the parameter is set , Therefore, the specific method of use has not changed .
This method of writing side effects in parameters , We will encounter a similar solution later （ Random numbers without side effects ）, And see in subsequent articles Monad How to solve such problems .
however , This article extends 「 Testability 」 The concept of , Generally speaking , Functions without side effects are absolutely measurable , And the test can be completed in the unit test phase . Functions with side effects / Methods make testing difficult . therefore , Through the concept of unit test and coverage , We can expose most of the problems before going online , It's very Fancy One way . If you add type derivation , The judgment of the availability of this system will be more perfect （ Of course , This is a Python This language is hard to do , But it can be based on
mypy Do something similar ）.
Of course , This is also the beginning of functional programming testing , We will introduce another unique testing concept of functional programming later —— Property based testing (Property-based testing), Then it introduces some good third-party modules and methods inspired by it .