Writing tests

Python has a number of different testing libraries available. The default library that comes with Python is unittest. This library is based on JUnit, from Java, and it shows. In this my opinion, the library is not especially user friendly, and there are 12 different tests to choose from, based on the expected outcome of the code. Writing the tests isn't especially intuitive for beginners, partly because of the amount of boilerplate code required just to work with unittest.

While nose2 is available as a third-party testing library, a more popular option is pytest. It requires no boilerplate; most of the time, just having pytest installed on your system is sufficient, though, in some cases, an explicit import of pytest is required. Tests are written as you would write normal Python code; the assert keyword tells the testing framework what the expected outcome is.

The easiest way to learn how to test is to see the code, which is especially useful in this case since pytest is so easy to use. The test file that we will cover in this section is separated into different sections for clarity, but they are all part of the same file.

It should be noted that, for pytest to work properly, all test files have to start with the word test_. In addition, the following test file contains arbitrary components; it is simply testing whether the class methods we have written perform as expected, prior to writing the actual simulation:

# test_functions.py (part 1)
1 """Assumes valves in series, with the first supplied by a tank 10 feet above the valve with a pipe length of 6 feet.
2 Water level is 4 feet above tank bottom; total water head = 14 feet.
3 """
4 from PipingSystems.pump.pump import CentrifPump, PositiveDisplacement
5 from PipingSystems.valve.valve import Gate, Globe, Relief
6​
7 valve1 = Gate("Valve 1", position=100, flow_coeff=200, sys_flow_in=utility_formulas.gravity_flow_rate(2, 1.67), press_in=utility_formulas.static_press(14))
8 pump1 = CentrifPump("Pump 1")
9 throttle1 = Globe("Throttle 1", position=100, flow_coeff=21)
10 valve2 = Gate("Valve 2", position=100, flow_coeff=200)
11 valve3 = Gate("Valve 3", position=100, flow_coeff=200)

Lines 1-3 provide some basic background assumptions for this test file. This helps when double-checking the results.

We have to import the items that we want to test, so the specific components are identified in lines 4 and 5.

Starting with line 7, we create instances of each component and provide the initial values. A position of 100 means that the valve is fully open. The flow coefficient represents how much the valve's constructions affects the flow that passes through it; a higher value indicates a lesser impact on the flow rate and pressure drop. Look at the following code:

# test_functions.py (part 2)
1 pump2 = PositiveDisplacement("Gear Pump", displacement=0.096, press_out=30)
2 relief1 = Relief("Relief 1", position=0, open_press=60, close_press=55)
3 recirc1 = Globe("Throttle 2", position=100, flow_coeff=21)
4 valve4 = Gate("Valve 4", position=100, flow_coeff=200)
5​
6 # Utility functions
7 def test_grav_flow():
8 flow_rate = utility_formulas.gravity_flow_rate(2, 1.67)
9 assert flow_rate == 319.28008077388426
10​
11​
12 def test_static_press():
13 press = utility_formulas.static_press(14)
14 assert press == 6.068373888888889

Lines 1-4 continue the creation of the component instances we will test. Starting with line 8, we define the functions that will test our utility formulas.

When using pytest, we define the test function (starting with the word test_), followed by the normal code logic that we expect to use in the final product (lines 8 and 13). Once all of the functionality has been defined, we create one or more assert statements that test the final outcome against its expected value (lines 9 and 15).

Depending on how you want to write your tests, you can accept the default precision of Python calculations, as shown in the following code, or you can truncate/round the results to the desired precision. A case could be made either way, as it doesn't require any extra effort to use the default, but if your final value is off by one value, the entire assertion errors out:

# test_functions.py (part 3)
class TestSystem:

# Gate Valve 1
def test_v1_press_in(self):
assert valve1.press_in == 6.068373888888889

​def test_v1_flow_in(self):
assert valve1.flow_in == 319.28008077388426

​def test_v1_flow_out(self):
valve1.flow_out = valve1.flow_in
assert valve1.flow_out == 319.28008077388426

In the preceding code listing, we can see an alternative way to use pytest. You can use individual functions, as demonstrated in part 2, or you can make a test class and define methods for each test case. This is useful if you have multiple tests that relate to each other. In this example, we are simply testing a single system with limited components; you could also have a separate class for each component type, different subsystems, and so on.

For this test suite, we will test the input/output flow rates and pressure values for the components, based on our expectations, as shown in the following code:

# test_functions.py (part 4)
def test_v1_press_drop(self):
valve1.press_drop(valve1.flow_out)
assert valve1.deltaP == 2.5484942494744516

def test_v1_press_out(self):
valve1.get_press_out(valve1.press_in)
assert valve1.press_out == 3.5198796394144374

# Centrifugal Pump
def test_pump1_input_press(self):
pump1.head_in = utility_formulas.press_to_head(valve1.press_out)
assert pump1.head_in == 8.119222584669064

Following code snippet is the part 5 of test_functions.py:

# test_functions.py (part 5)
def test_pump1_start_pump(self):
pump1.start_pump(1750, 50, 16)
assert pump1.speed == 1750
assert pump1.flow == 50.0
assert pump1.outlet_pressure == 16
assert pump1.power == 0.11770474358069433

​# Globe valve 1
def test_t1_press_in(self):
throttle1.press_in = pump1.outlet_pressure
assert throttle1.press_in == 16

In the preceding code listing, we can see that multiple assert statements can be used within a single test case. Here, once a pump is started, we want to ensure that all of the pump parameters are set correctly. Rather than writing a separate test case for each condition, we can put all assertions into one case, as shown in the previous code. The following code snippet is part 6 of test_functions.py

# test_functions.py (part 6)
def test_t1_flow_in(self):
throttle1.flow_in = pump1.flow
assert throttle1.flow_in == 50.0

​def test_t1_flow_out(self):
throttle1.flow_out = throttle1.flow_in
assert throttle1.flow_out == 50.0

​def test_t1_press_drop(self):
throttle1.press_drop(throttle1.flow_out)
assert throttle1.deltaP == 5.668934240362812

​def test_t1_press_out(self):
throttle1.get_press_out(throttle1.press_in)
assert throttle1.press_out == 10.331065759637188

Following code snippet is the part 7 of test_functions.py:

# test_functions.py (part 7)
# Gate Valve 2
def test_v2_input_press(self):
valve2.press_in = throttle1.press_out
assert valve2.press_in == 10.331065759637188

​def test_v2_input_flow(self):
valve2.flow_in = throttle1.flow_out
assert valve2.flow_in == 50.0


def test_v2_output_flow(self):
valve2.flow_out = valve2.flow_in
assert valve2.flow_out == 50.0

Following code snippet is the part 8 of test_functions.py:

# test_functions.py (part 8)
def test_v2_press_drop(self):
valve2.press_drop(valve2.flow_out)
assert valve2.deltaP == 0.0625


def test_v2_press_out(self):
valve2.get_press_out(valve2.press_in)
assert valve2.press_out == 10.268565759637188

# Gate Valve 3
def test_v3_input_press(self):
valve3.press_in = valve2.press_out
assert valve3.press_in == 10.268565759637188

Following code snippet is the part 9 of test_functions.py:

# test_functions.py (part 9)
def test_v3_input_flow(self):
valve3.flow_in = valve2.flow_out
assert valve3.flow_in == 50.0

def test_v3_output_flow(self):
valve3.flow_out = valve3.flow_in
assert valve3.flow_out == 50.0

def test_v3_press_drop(self):
valve3.press_drop(valve3.flow_out)
assert valve3.deltaP == 0.0625

def test_v3_press_out(self):
valve3.get_press_out(valve3.press_in)
assert valve3.press_out == 10.206065759637188

Following code snippet is the part 10 of test_functions.py:

# test_functions.py (part 10)
# Gear Pump
def test_pump2_input_press(self):
pump2.head_in = utility_formulas.press_to_head(valve3.press_out)
assert pump2.head_in == 23.542088964737797

​def test_pump2_output(self):
pump2.adjust_speed(300)
assert pump2.speed == 300
assert pump2.flow == 28.8
assert pump2.power == 0.10753003776038036

​# Relief Valve 1
def test_relief1_input_press(self):
relief1.press_in = pump2.outlet_pressure
assert relief1.press_in == 30

Following code snippet is the part 11 of test_functions.py:

# test_functions.py (part 11)
# Globe Valve 2
def test_t2_input_press(self):
recirc1.press_in = pump2.outlet_pressure
assert recirc1.press_in == 30

​def test_t2_input_flow(self):
recirc1.flow_in = pump2.flow
assert recirc1.flow_in == 28.8

​def test_t2_output_flow(self):
recirc1.flow_out = recirc1.flow_in
assert recirc1.flow_out == 28.8

​def test_2_press_drop(self):
recirc1.press_drop(recirc1.flow_out)
assert recirc1.deltaP == 1.8808163265306124

Following code snippet is the part 12 of test_functions.py:

# test_functions.py (part 12)
def test_t2_press_out(self):
recirc1.get_press_out(recirc1.press_in)
assert recirc1.press_out == 28.119183673469387

​# Gate Valve 4
def test_v4_input_press(self):
valve4.press_in = recirc1.press_out
assert valve4.press_in == 28.119183673469387

​def test_v4_input_flow(self):
valve4.flow_in = recirc1.flow_out
assert valve4.flow_in == 28.8

Following code snippet is the part 13 of test_functions.py:

# test_functions.py (part 13)
def test_v4_output_flow(self):
valve4.flow_out = valve4.flow_in
assert valve4.flow_out == 28.8

​def test_v4_press_drop(self):
valve4.press_drop(valve4.flow_out)
assert valve4.deltaP == 0.020736000000000004

​def test_v4_press_out(self):
valve4.get_press_out(valve4.press_in)
assert valve4.press_out == 28.098447673469387

Assuming that we have written the tests correctly, this will be completed successfully. The following screenshot shows successful completion:

pytest success

Each of the dots represents a test case that has successfully passed. We also receive information on the total number of tests that were processed, as well as the total time required to perform all of the tests.

If we had a problem, we would see something like the following screenshot:

pytest failure

The pytest library is helpful in telling us the problem. First, where we saw a row of dots previously, a capital F shows where the failure occurred among all of the test cases.

Second, a FAILURES section provides the details. In this instance, it tells us the test case that failed (test_grav_flow()), the specific assertion statement that failed, and what the actual received result was compared to the expected value. The expected value is what we have written into the test case; in this case, we deleted the final number from the calculated flow rate, resulting in the error.

There are many other tests that can be written, such as testing a generic pump instance, and testing a specific gate valve instance. The following screenshot shows how to run multiple test files at one time (these test files are provided in the code repository for this book):

Multiple pytest cases

With the preceding test run, we pointed pytest at a directory that contained the test files we wanted to run. As shown in the test results, 149 test cases were found. The pytest library will process all of the tests within each file, providing a running count of the total percentage completed after each file.

One final thing to point out about tests refers back to re-raising exceptions in our code. When an exception is re-raised after catching it, it allows a test case to capture the exception and run an assert statement against it, just like the calculations we've tested. The following code listing provides an example of this (this is just one test method from the entire test file):

def test_tank_level_str(self):
tank1 = Tank()
with pytest.raises(TypeError) as excinfo:
tank1.level = "a"
exception_msg = excinfo.value.args[0]
assert exception_msg == "Numeric values only."

In this test, we want to check whether an exception is raised if a non-numeric value is provided as an argument to the tank-level method in tank.py. Doing this allows us to ensure that the correct exception is generated, as well as the error message that is printed, thereby allowing multiple exceptions with different messages to be tested.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.142.2