Skip to content

Commit a3e05f2

Browse files
wholmgrencwhanse
authored andcommitted
use pytest-mock to simplify and improve tests, improve test docs (#468)
use pytest-mock to simplify and improve tests, improve test docs
1 parent 8aec267 commit a3e05f2

File tree

10 files changed

+311
-138
lines changed

10 files changed

+311
-138
lines changed

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ install:
2828
- cmd: conda info -a
2929

3030
# install depenencies
31-
- cmd: conda create -n test_env --yes --quiet python=%PYTHON_VERSION% pip numpy scipy pytables pandas nose pytest pytz ephem numba siphon -c conda-forge
31+
- cmd: conda create -n test_env --yes --quiet python=%PYTHON_VERSION% pip numpy scipy pytables pandas nose pytest pytz ephem numba siphon pytest-mock -c conda-forge
3232
- cmd: activate test_env
3333
- cmd: python --version
3434
- cmd: conda list

ci/requirements-py27-min.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dependencies:
44
- pytz
55
- pytest
66
- pytest-cov
7+
- pytest-mock
78
- nose
89
- pip:
910
- coveralls

ci/requirements-py27.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
- siphon
1515
- pytest
1616
- pytest-cov
17+
- pytest-mock
1718
- nose
1819
- pip:
1920
- coveralls

ci/requirements-py34.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
- siphon
1515
- pytest
1616
- pytest-cov
17+
- pytest-mock
1718
- nose
1819
- pip:
1920
- coveralls

ci/requirements-py35.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
- siphon
1515
- pytest
1616
- pytest-cov
17+
- pytest-mock
1718
- nose
1819
- pip:
1920
- coveralls

ci/requirements-py36.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
#- siphon
1515
- pytest
1616
- pytest-cov
17+
- pytest-mock
1718
- nose
1819
- pip:
1920
- coveralls

docs/sphinx/source/contributing.rst

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ contribute.
99

1010

1111
Easy ways to contribute
12-
-----------------------
12+
~~~~~~~~~~~~~~~~~~~~~~~
1313

1414
Here are a few ideas for you can contribute, even if you are new to
1515
pvlib-python, git, or Python:
@@ -33,7 +33,7 @@ pvlib-python, git, or Python:
3333

3434

3535
How to contribute new code
36-
--------------------------
36+
~~~~~~~~~~~~~~~~~~~~~~~~~~
3737

3838
Contributors to pvlib-python use GitHub's pull requests to add/modify
3939
its source code. The GitHub pull request process can be intimidating for
@@ -81,29 +81,142 @@ changes, such as fixing documentation typos.
8181

8282

8383
Testing
84-
-------
84+
~~~~~~~
8585

8686
pvlib's unit tests can easily be run by executing ``py.test`` on the
8787
pvlib directory:
8888

89-
``py.test pvlib``
89+
``pytest pvlib``
9090

9191
or, for a single module:
9292

93-
``py.test pvlib/test/test_clearsky.py``
93+
``pytest pvlib/test/test_clearsky.py``
9494

95-
While copy/paste coding should generally be avoided, it's a great way
96-
to learn how to write unit tests!
95+
or, for a single test:
9796

98-
Unit test code should be placed in the corresponding test module in the
99-
pvlib/test directory.
97+
``pytest pvlib/test/test_clearsky.py::test_ineichen_nans``
98+
99+
Use the ``--pdb`` flag to debug failures and avoid using ``print``.
100+
101+
New unit test code should be placed in the corresponding test module in
102+
the pvlib/test directory.
100103

101104
Developers **must** include comprehensive tests for any additions or
102105
modifications to pvlib.
103106

107+
pvlib-python contains 3 "layers" of code: functions, PVSystem/Location,
108+
and ModelChain. Contributors will need to add tests that correspond to
109+
the layer that they modify.
110+
111+
Functions
112+
---------
113+
Tests of core pvlib functions should ensure that the function returns
114+
the desired output for a variety of function inputs. The tests should be
115+
independent of other pvlib functions (see :issue:`394`). The tests
116+
should ensure that all reasonable combinations of input types (floats,
117+
nans, arrays, series, scalars, etc) work as expected. Remember that your
118+
use case is likely not the only way that this function will be used, and
119+
your input data may not be generic enough to fully test the function.
120+
Write tests that cover the full range of validity of the algorithm.
121+
It is also important to write tests that assert the return value of the
122+
function or that the function throws an exception when input data is
123+
beyond the range of algorithm validity.
124+
125+
PVSystem/Location
126+
-----------------
127+
The PVSystem and Location classes provide convenience wrappers around
128+
the core pvlib functions. The tests in test_pvsystem.py and
129+
test_location.py should ensure that the method calls correctly wrap the
130+
function calls. Many PVSystem/Location methods pass one or more of their
131+
object's attributes (e.g. PVSystem.module_parameters, Location.latitude)
132+
to a function. Tests should ensure that attributes are passed correctly.
133+
These tests should also ensure that the method returns some reasonable
134+
data, though the precise values of the data should be covered by
135+
function-specific tests discussed above.
136+
137+
We prefer to use the ``pytest-mock`` framework to write these tests. The
138+
test below shows an example of testing the ``PVSystem.ashraeiam``
139+
method. ``mocker`` is a ``pytest-mock`` object. ``mocker.spy`` adds
140+
features to the ``pvsystem.ashraeiam`` *function* that keep track of how
141+
it was called. Then a ``PVSystem`` object is created and the
142+
``PVSystem.ashraeiam`` *method* is called in the usual way. The
143+
``PVSystem.ashraeiam`` method is supposed to call the
144+
``pvsystem.ashraeiam`` function with the angles supplied to the method
145+
call and the value of ``b`` that we defined in ``module_parameters``.
146+
The ``pvsystem.ashraeiam.assert_called_once_with`` tests that this does,
147+
in fact, happen. Finally, we check that the output of the method call is
148+
reasonable.
149+
150+
.. code-block:: python
151+
def test_PVSystem_ashraeiam(mocker):
152+
# mocker is a pytest-mock object.
153+
# mocker.spy adds code to a function to keep track of how it is called
154+
mocker.spy(pvsystem, 'ashraeiam')
155+
156+
# set up inputs
157+
module_parameters = {'b': 0.05}
158+
system = pvsystem.PVSystem(module_parameters=module_parameters)
159+
thetas = 1
160+
161+
# call the method
162+
iam = system.ashraeiam(thetas)
163+
164+
# did the method call the function as we expected?
165+
# mocker.spy added assert_called_once_with to the function
166+
pvsystem.ashraeiam.assert_called_once_with(thetas, b=module_parameters['b'])
167+
168+
# check that the output is reasonable, but no need to duplicate
169+
# the rigorous tests of the function
170+
assert iam < 1.
171+
172+
Avoid writing PVSystem/Location tests that depend sensitively on the
173+
return value of a statement as a substitute for using mock. These tests
174+
are sensitive to changes in the functions, which is *not* what we want
175+
to test here, and are difficult to maintain.
176+
177+
ModelChain
178+
----------
179+
The tests in test_modelchain.py should ensure that
180+
``ModelChain.__init__`` correctly configures the ModelChain object to
181+
eventually run the selected models. A test should ensure that the
182+
appropriate method is actually called in the course of
183+
``ModelChain.run_model``. A test should ensure that the model selection
184+
does have a reasonable effect on the subsequent calculations, though the
185+
precise values of the data should be covered by the function tests
186+
discussed above. ``pytest-mock`` can also be used for testing ``ModelChain``.
187+
188+
The example below shows how mock can be used to assert that the correct
189+
PVSystem method is called through ``ModelChain.run_model``.
190+
191+
.. code-block:: python
192+
def test_modelchain_dc_model(mocker):
193+
# set up location and system for model chain
194+
location = location.Location(32, -111)
195+
system = pvsystem.PVSystem(module_parameters=some_sandia_mod_params,
196+
inverter_parameters=some_cecinverter_params)
197+
198+
# mocker.spy adds code to the system.sapm method to keep track of how
199+
# it is called. use returned mock object m to make assertion later,
200+
# but see example above for alternative
201+
m = mocker.spy(system, 'sapm')
202+
203+
# make and run the model chain
204+
mc = ModelChain(system, location,
205+
aoi_model='no_loss', spectral_model='no_loss')
206+
times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
207+
mc.run_model(times)
208+
209+
# assertion fails if PVSystem.sapm is not called once
210+
# if using returned m, prefer this over m.assert_called_once()
211+
# for compatibility with python < 3.6
212+
assert m.call_count == 1
213+
214+
# ensure that dc attribute now exists and is correct type
215+
assert isinstance(mc.dc, (pd.Series, pd.DataFrame))
216+
104217
105218
This documentation
106-
------------------
219+
~~~~~~~~~~~~~~~~~~
107220

108221
If this documentation is unclear, help us improve it! Consider looking
109222
at the `pandas

docs/sphinx/source/whatsnew/v0.6.0.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,16 @@ Bug fixes
2424

2525
Documentation
2626
~~~~~~~~~~~~~
27+
* Expand testing section with guidelines for functions, PVSystem/Location
28+
objects, and ModelChain.
2729

2830

2931
Testing
3032
~~~~~~~
33+
* Add pytest-mock dependency
34+
* Use pytest-mock to ensure that PVSystem methods call corresponding functions
35+
correctly. Removes implicit dependence on precise return values of functions
36+
* Use pytest-mock to ensure that ModelChain DC model is set up correctly.
3137

3238

3339
Contributors

pvlib/test/test_modelchain.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,23 @@ def test_dc_models(system, cec_dc_snl_ac_system, pvwatts_dc_pvwatts_ac_system,
211211
assert_series_equal(ac, expected, check_less_precise=2)
212212

213213

214+
@requires_scipy
215+
@pytest.mark.parametrize('dc_model', ['sapm', 'singlediode', 'pvwatts_dc'])
216+
def test_infer_dc_model(system, cec_dc_snl_ac_system,
217+
pvwatts_dc_pvwatts_ac_system, location, dc_model,
218+
mocker):
219+
dc_systems = {'sapm': system, 'singlediode': cec_dc_snl_ac_system,
220+
'pvwatts_dc': pvwatts_dc_pvwatts_ac_system}
221+
system = dc_systems[dc_model]
222+
m = mocker.spy(system, dc_model)
223+
mc = ModelChain(system, location,
224+
aoi_model='no_loss', spectral_model='no_loss')
225+
times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
226+
mc.run_model(times)
227+
assert m.call_count == 1
228+
assert isinstance(mc.dc, (pd.Series, pd.DataFrame))
229+
230+
214231
def acdc(mc):
215232
mc.ac = mc.dc
216233

0 commit comments

Comments
 (0)