Skip to content

Commit 33ca83f

Browse files
committed
test: add full unit test coverage for simple_metric_config
The `simple_metric_config` is slightly refactored. Documentation is much more thorough and clear about both purpose and functionality. Unit tests not only cover various field types, but also explicitly thrown exceptions as well as the internal parameters cache.
1 parent 00f19ba commit 33ca83f

7 files changed

Lines changed: 606 additions & 24 deletions

classes/local/testing/metric_settable_values.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
*
4040
* **TESTING ONLY: This exists purely to run unit tests.**
4141
*
42+
* @codeCoverageIgnore
43+
*
4244
* @package tool_monitoring
4345
* @copyright 2025 MootDACH DevCamp
4446
* Daniel Fainberg <d.fainberg@tu-berlin.de>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* Definition of the {@see testing_simple_metric_config} class.
19+
*
20+
* @package tool_monitoring
21+
* @copyright 2025 MootDACH DevCamp
22+
* Daniel Fainberg <d.fainberg@tu-berlin.de>
23+
* Martin Gauk <martin.gauk@tu-berlin.de>
24+
* Sebastian Rupp <sr@artcodix.com>
25+
* Malte Schmitz <mal.schmitz@uni-luebeck.de>
26+
* Melanie Treitinger <melanie.treitinger@ruhr-uni-bochum.de>
27+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28+
*/
29+
30+
namespace tool_monitoring\local\testing;
31+
32+
use stdClass;
33+
use tool_monitoring\simple_metric_config;
34+
35+
/**
36+
* Extension of the {@see simple_metric_config} class for testing purposes.
37+
*
38+
* **TESTING ONLY: This exists purely to run unit tests.**
39+
* Some properties and constructor parameters do not make sense in a real-world context. They are used to test certain methods and
40+
* the goal of this class is to cover all code paths.
41+
*
42+
* @codeCoverageIgnore
43+
*
44+
* @package tool_monitoring
45+
* @copyright 2025 MootDACH DevCamp
46+
* Daniel Fainberg <d.fainberg@tu-berlin.de>
47+
* Martin Gauk <martin.gauk@tu-berlin.de>
48+
* Sebastian Rupp <sr@artcodix.com>
49+
* Malte Schmitz <mal.schmitz@uni-luebeck.de>
50+
* Melanie Treitinger <melanie.treitinger@ruhr-uni-bochum.de>
51+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
52+
*/
53+
class testing_simple_metric_config extends simple_metric_config {
54+
/** @var string Not promoted from the constructor. */
55+
public string $notpromotedstring;
56+
57+
/**
58+
* @var string Something protected.
59+
* {@noinspection PhpUnused}
60+
*/
61+
protected string $somethingprotected = 'protected';
62+
63+
/**
64+
* @var string Something private.
65+
* {@noinspection PhpUnusedPrivateFieldInspection}
66+
*/
67+
private string $somethingprivate = 'private';
68+
69+
/**
70+
* Constructor with some variants for testing.
71+
*
72+
* @param string $publicstringrequired Required string; promoted to public property.
73+
* @param stdClass $publicobj Object with a default; promoted to public property.
74+
* @param int $protectedint Integer with a default; promoted to protected property.
75+
* @param float $privatereadonlyfloat Float with a default; promoted to private property.
76+
* @param bool $publicbool Boolean with a default; promoted to public property.
77+
* @param array|string|null $publicunion Union of types with a default; promoted to public property.
78+
* @param string $notpromotedstring String with a default; not promoted to any property.
79+
*
80+
* {@noinspection PhpPropertyOnlyWrittenInspection}
81+
*/
82+
public function __construct(
83+
/** @var string Public string. */
84+
public string $publicstringrequired,
85+
/** @var stdClass Public object. */
86+
public stdClass $publicobj = new stdClass(),
87+
/** @var int Protected integer. */
88+
protected int $protectedint = 42,
89+
/** @var float Private float. */
90+
private readonly float $privatereadonlyfloat = 3.14,
91+
/** @var bool Public boolean. */
92+
public bool $publicbool = true,
93+
/** @var array|string|null Public union. */
94+
public array|string|null $publicunion = null,
95+
string $notpromotedstring = 'bar',
96+
) {
97+
$this->notpromotedstring = $notpromotedstring;
98+
}
99+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* Definition of the {@see testing_simple_metric_config_cache} class.
19+
*
20+
* @package tool_monitoring
21+
* @copyright 2025 MootDACH DevCamp
22+
* Daniel Fainberg <d.fainberg@tu-berlin.de>
23+
* Martin Gauk <martin.gauk@tu-berlin.de>
24+
* Sebastian Rupp <sr@artcodix.com>
25+
* Malte Schmitz <mal.schmitz@uni-luebeck.de>
26+
* Melanie Treitinger <melanie.treitinger@ruhr-uni-bochum.de>
27+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28+
*/
29+
30+
namespace tool_monitoring\local\testing;
31+
32+
use tool_monitoring\simple_metric_config;
33+
34+
/**
35+
* Extension of the {@see simple_metric_config} class for testing purposes.
36+
*
37+
* **TESTING ONLY: This exists purely to run unit tests.**
38+
*
39+
* @codeCoverageIgnore
40+
*
41+
* @package tool_monitoring
42+
* @copyright 2025 MootDACH DevCamp
43+
* Daniel Fainberg <d.fainberg@tu-berlin.de>
44+
* Martin Gauk <martin.gauk@tu-berlin.de>
45+
* Sebastian Rupp <sr@artcodix.com>
46+
* Malte Schmitz <mal.schmitz@uni-luebeck.de>
47+
* Melanie Treitinger <melanie.treitinger@ruhr-uni-bochum.de>
48+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49+
*/
50+
class testing_simple_metric_config_cache extends simple_metric_config {
51+
/**
52+
* Constructor without additional logic.
53+
*
54+
* @param string $foo Foo
55+
* @param int $spam Spam
56+
*
57+
* @phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace
58+
*/
59+
public function __construct(
60+
/** @var string Foo */
61+
public readonly string $foo = 'bar',
62+
/** @var int Spam */
63+
public readonly int $spam = 1234567,
64+
) {}
65+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
// This file is part of Moodle - http://moodle.org/
3+
//
4+
// Moodle is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Moodle is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU General Public License
15+
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16+
17+
/**
18+
* Definition of the {@see testing_simple_metric_config_missing_constructor} class.
19+
*
20+
* @package tool_monitoring
21+
* @copyright 2025 MootDACH DevCamp
22+
* Daniel Fainberg <d.fainberg@tu-berlin.de>
23+
* Martin Gauk <martin.gauk@tu-berlin.de>
24+
* Sebastian Rupp <sr@artcodix.com>
25+
* Malte Schmitz <mal.schmitz@uni-luebeck.de>
26+
* Melanie Treitinger <melanie.treitinger@ruhr-uni-bochum.de>
27+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28+
*/
29+
30+
namespace tool_monitoring\local\testing;
31+
32+
use tool_monitoring\simple_metric_config;
33+
34+
/**
35+
* Extension of the {@see simple_metric_config} class for testing purposes.
36+
*
37+
* **TESTING ONLY: This exists purely to run unit tests.**
38+
*
39+
* @codeCoverageIgnore
40+
*
41+
* @package tool_monitoring
42+
* @copyright 2025 MootDACH DevCamp
43+
* Daniel Fainberg <d.fainberg@tu-berlin.de>
44+
* Martin Gauk <martin.gauk@tu-berlin.de>
45+
* Sebastian Rupp <sr@artcodix.com>
46+
* Malte Schmitz <mal.schmitz@uni-luebeck.de>
47+
* Melanie Treitinger <melanie.treitinger@ruhr-uni-bochum.de>
48+
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
49+
*/
50+
class testing_simple_metric_config_missing_constructor extends simple_metric_config {
51+
/**
52+
* @var string Something public.
53+
* {@noinspection PhpUnused}
54+
*/
55+
public string $somethingpublic = 'public';
56+
57+
/**
58+
* @var string Something protected.
59+
* {@noinspection PhpUnused}
60+
*/
61+
protected string $somethingprotected = 'protected';
62+
63+
/**
64+
* @var string Something private.
65+
* {@noinspection PhpUnusedPrivateFieldInspection}
66+
*/
67+
private string $somethingprivate = 'private';
68+
}

classes/simple_metric_config.php

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,20 @@
4545
use tool_monitoring\form\config as config_form;
4646

4747
/**
48-
* Basic implementation of the {@see metric_config} interface.
48+
* Helper base class for simple metric configurations; fully implements the **{@see metric_config}** interface.
4949
*
50-
* A concrete subclass must simply define a constructor with
51-
* {@link https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion promoted parameters}.
52-
* Assuming all of those are public and there are no additional public properties on the config object, the JSON (de-)serialization
53-
* simply maps those properties to keys in a JSON object.
50+
* During JSON serialization, the **public** properties of an instance are turned into the JSON object (saved in the DB).
51+
* See the {@see self::jsonSerialize `jsonSerialize`} method for details.
52+
* For JSON deserialization, the JSON object is expected to provide **keys that map to the constructor parameters** of the class.
53+
* See the {@see self::from_json `from_json`} method for details.
5454
*
55-
* The definition of the Moodle form fields and their validation logic are inferred from those properties as well.
56-
* For translatable field labels, a string with the ID `metric:<config-class-name>:<property-name>` must exist in the component
57-
* defining the config class. If additionally a string with the ID `metric:<config-class-name>:<property-name>_help` exists,
58-
* a help button is added to the form field with the corresponding text.
55+
* The same logic applies to the {@see self::to_form_data `to_form_data`} and {@see self::with_form_data `with_form_data`} methods.
56+
* The former returns the **public** properties of a config instance, the latter expects the form data to have properties that
57+
* **map to the constructor parameters** of the class.
5958
*
60-
* Consider the following example config class defined in a plugin called `local_example`:
59+
* This means, a concrete subclass can be simply defined as a dataclass that has a constructor with public
60+
* {@link https://www.php.net/manual/en/language.oop5.decon.php#language.oop5.decon.constructor.promotion promoted parameters}.
61+
* For example:
6162
*
6263
* ```
6364
* class my_metric_config extends simple_metric_config {
@@ -72,21 +73,34 @@
7273
*
7374
* Resulting/expected form data: `['foo' => 'bar', 'spam' => '3.14']`.
7475
*
75-
* The Moodle form definition would be similar to this:
76+
* The definition of the Moodle form fields and their validation logic are inferred from those properties as well.
77+
* For translatable field labels, a string with the ID `metric:<config-class-name>:<property-name>` must exist in the component
78+
* defining the config class. If additionally a string with the ID `metric:<config-class-name>:<property-name>_help` exists,
79+
* a help button is added to the form field with the corresponding text.
80+
*
81+
* In a `local_example` plugin, the Moodle form definition for the example config above would be similar to this:
82+
*
7683
* ```
7784
* $mform->addElement('text', 'foo', get_string('metric:my_metric_config:foo', 'local_example'));
7885
* $mform->addHelpButton('foo', 'metric:my_metric_config:foo', 'local_example');
7986
* $mform->setType('foo', PARAM_TEXT);
80-
* $mform->addRule('foo', null, 'required', null, 'client');
8187
*
8288
* $mform->addElement('text', 'spam', get_string('metric:my_metric_config:spam', 'local_example'));
8389
* $mform->addHelpButton('spam', 'metric:my_metric_config:spam', 'local_example');
8490
* $mform->setType('spam', PARAM_FLOAT);
8591
* $mform->addRule('spam', null, 'numeric', null, 'client');
86-
* $mform->addRule('spam', null, 'required', null, 'client');
8792
* ```
8893
*
89-
* For more advanced definition/validation options, override {@see extend_form_definition} and {@see extend_form_validation}.
94+
* The automatic field inference currently supports the following constructor parameter types:
95+
* - `bool` gives an `advcheckbox`/`PARAM_BOOL` field.
96+
* - `float` gives a `text`/`PARAM_FLOAT` field with client-side numeric validation.
97+
* - `int` gives a `text`/`PARAM_INT` field with client-side numeric validation.
98+
* - `string` gives a `text`/`PARAM_TEXT` field.
99+
*
100+
* Any other type annotation is treated as `string`, which results in a `text`/`PARAM_TEXT` field without any validation.
101+
*
102+
* For more advanced definition and validation options, the {@see self::extend_form_definition `extend_form_definition`} and
103+
* {@see self::extend_form_validation `extend_form_validation`} methods can still be overridden/extended.
90104
*
91105
* @package tool_monitoring
92106
* @copyright 2025 MootDACH DevCamp
@@ -98,20 +112,20 @@
98112
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
99113
*/
100114
abstract class simple_metric_config implements metric_config {
101-
/** @var array<string, array<string, ReflectionParameter>> Cache for config parameters indexed by config class name. */
102-
private static array $configparameters = [];
115+
/** @var array<string, array<string, ReflectionParameter>> Cache for constructor parameters indexed by config class name. */
116+
private static array $constructorparameters = [];
103117

104118
/**
105-
* Reflects the class, analyzes the constructor of the config class, and returns all parameters indexed by name.
119+
* Reflects the calling class, analyzes its constructor, and returns all parameters indexed by name.
106120
*
107121
* Caches the result after the first call.
108122
*
109123
* @return array<string, ReflectionParameter> Constructor parameters indexed by name.
110124
* @throws simple_metric_config_constructor_missing
111125
*/
112-
protected static function get_config_parameters(): array {
113-
if (isset(self::$configparameters[static::class])) {
114-
return self::$configparameters[static::class];
126+
private static function get_constructor_parameters(): array {
127+
if (isset(self::$constructorparameters[static::class])) {
128+
return self::$constructorparameters[static::class];
115129
}
116130
$class = new ReflectionClass(static::class);
117131
if (is_null($constructor = $class->getConstructor())) {
@@ -122,7 +136,7 @@ protected static function get_config_parameters(): array {
122136
column_key: null,
123137
index_key: 'name',
124138
);
125-
self::$configparameters[static::class] = $parameters;
139+
self::$constructorparameters[static::class] = $parameters;
126140
return $parameters;
127141
}
128142

@@ -154,7 +168,7 @@ public static function from_json(string $json): static {
154168
throw new json_invalid();
155169
}
156170
$args = [];
157-
foreach (self::get_config_parameters() as $name => $param) {
171+
foreach (self::get_constructor_parameters() as $name => $param) {
158172
if (array_key_exists($name, $data)) {
159173
$args[$name] = $data[$name];
160174
} else if (!$param->isOptional()) {
@@ -177,7 +191,7 @@ public static function from_json(string $json): static {
177191
#[\Override]
178192
public static function with_form_data(stdClass $formdata): static {
179193
$args = [];
180-
foreach (self::get_config_parameters() as $name => $param) {
194+
foreach (self::get_constructor_parameters() as $name => $param) {
181195
if (property_exists($formdata, $name)) {
182196
$args[$name] = $formdata->$name;
183197
} else if (!$param->isOptional()) {
@@ -230,8 +244,11 @@ public static function extend_form_definition(config_form $configform, MoodleQui
230244
$cls = substr($cls, $pos + 1);
231245
}
232246
$stringmanager = get_string_manager();
233-
foreach (self::get_config_parameters() as $paramname => $param) {
247+
foreach (self::get_constructor_parameters() as $paramname => $param) {
234248
$labelid = "metric:$cls:$paramname";
249+
if (PHPUNIT_TEST) {
250+
$labelid = "testing:$labelid";
251+
}
235252
self::add_field_to_form($mform, $param, new lang_string($labelid, $component));
236253
// Optionally, add a help button if the defining component has a corresponding language string.
237254
if ($stringmanager->string_exists("{$labelid}_help", $component)) {

lang/en/tool_monitoring.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,12 @@
7979

8080
$string['tagarea_metrics'] = 'Metrics';
8181
$string['tagcollection_monitoring'] = 'Monitoring';
82+
83+
$string['testing:metric:testing_simple_metric_config:notpromotedstring'] = 'String with a default; not promoted to any property.';
84+
$string['testing:metric:testing_simple_metric_config:privatereadonlyfloat'] = 'Float with a default; promoted to private property.';
85+
$string['testing:metric:testing_simple_metric_config:protectedint'] = 'Integer with a default; promoted to protected property.';
86+
$string['testing:metric:testing_simple_metric_config:publicbool'] = 'Boolean with a default; promoted to public property.';
87+
$string['testing:metric:testing_simple_metric_config:publicobj'] = 'Object with a default; promoted to public property.';
88+
$string['testing:metric:testing_simple_metric_config:publicstringrequired'] = 'Required string; promoted to public property.';
89+
$string['testing:metric:testing_simple_metric_config:publicstringrequired_help'] = 'Help text for the above.';
90+
$string['testing:metric:testing_simple_metric_config:publicunion'] = 'Union of types with a default; promoted to public property.';

0 commit comments

Comments
 (0)