@@ -9,7 +9,7 @@ contribute.
99
1010
1111Easy ways to contribute
12- -----------------------
12+ ~~~~~~~~~~~~~~~~~~~~~~~
1313
1414Here are a few ideas for you can contribute, even if you are new to
1515pvlib-python, git, or Python:
@@ -33,7 +33,7 @@ pvlib-python, git, or Python:
3333
3434
3535How to contribute new code
36- --------------------------
36+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
3737
3838Contributors to pvlib-python use GitHub's pull requests to add/modify
3939its source code. The GitHub pull request process can be intimidating for
@@ -81,29 +81,142 @@ changes, such as fixing documentation typos.
8181
8282
8383Testing
84- -------
84+ ~~~~~~~
8585
8686pvlib's unit tests can easily be run by executing ``py.test `` on the
8787pvlib directory:
8888
89- ``py.test pvlib ``
89+ ``pytest pvlib ``
9090
9191or, 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
101104Developers **must ** include comprehensive tests for any additions or
102105modifications 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
108221If this documentation is unclear, help us improve it! Consider looking
109222at the `pandas
0 commit comments