Re: CPANifying our test framework - or parts of it

[email protected] (Sam Kington) Sat, 10 Sep 2016 17:39:37 +0200
Newsgroups perl.qa
Message-ID <[email protected]>
Thanks for the detailed response!

On 10 Sep 2016, at 15:42, James E Keenan <[email protected]> wrote:
> Let me state my overall impression at the output.  It's apparent that =
many (hundreds?) of hours of work have been put into the development of =
this testing apparatus, but if it were put on CPAN I doubt I would use =
it.  It appears to be too heavily designed toward your specific use =
cases and, in certain respects, is designed in a way that does not sit =
well with me.


Yes, there will inevitably have to be a fair bit of work done to make it =
more general-purpose. (=46rom our purposes, a general-purpose =E2=80=9Ctes=
t=E2=80=9D method was fine because we have no other code that has a =
conflicting method, for instance, but obviously this would need =
revamping for the CPAN.) If nothing else regarding documentation, as I =
don=E2=80=99t think I managed to explain very well what exactly this =
test framework does.

> On 09/09/2016 09:34 PM, Sam Kington wrote:
>> At its most elaborate, our current test code lets you say e.g.
>>=20
>> # Assume $self->state('location_id') has been set previously
>> $self->test(
>=20
> Of what class is $self an instance?  The class of the object being =
tested (as inferred from the existence of a 'state' method)?  Or a =
specialized testing class (as inferred from the existence of a 'test' =
method)?
>=20
> It seems that you are mixing both real object data with testing data =
in the same object.  Speaking for myself, I don't think that's a clean =
design.

It=E2=80=99s a test class. The state method is to remember data returned =
by previous test results, so e.g. you can inline them in URLs and/or =
include them in subsequent tests.

>>    expect =3D> {
>>        http_code =3D> HTTP_CREATED,
>>        sql_count =3D> 12,
>>        data      =3D> {
>>            location_id =3D> $self->state('location_id'),
>>            build_id    =3D> qr{^ (?<build_id> BUILD \d+ ) $}x,
>>            built       =3D> $self->true,
>>            author     =3D> $self->meh,    # It's OK if they call him =
Idiot.
>>            structures =3D> [
>>                {
>>                    _hashref_contains =3D> {
>>                        structure_id =3D> qr{^ (?<structure_id> STRUCT =
\d+ ) $}x,
>>                        type         =3D> 'dolmen',
>>                        material     =3D> 'concrete',
>>=20
>>                        # There's probably stuff about where the =
dolmen
>>                        # was erected but we ignore that for the =
purpose
>>                        # of this test.
>>                    }
>>                }
>>            ]
>>        }
>>    }
>> );
>=20
> More important is the fact that you must have a lot of code under the =
hood to generate the hash reference which is the value of the 'expect' =
key. That code is inquiring about many different attributes of the =
object and it has to get *every one* of those attributes right for the =
unit test to pass.  You're making *one, big assertion* about the state =
of the object -- and you're probably making a strong assumption about =
the internal structure of the object.

There=E2=80=99s no objects involved here. The test says =E2=80=9Cif I =
call this particular URL, I expect to get back a hashref of data which =
matches this description=E2=80=9D. The description can be as complicated =
or as simple as the test requires.

I should add that when the code runs, it generates (in a subtest) =
individual tests for each component of the data structure, so if one of =
the keys didn=E2=80=99t match, but the rest of the data structure was =
fine, you=E2=80=99d get plenty of passing tests and then one failure, =
e.g. =E2=80=9Cnot ok 123 data{built} is true=E2=80=9D, and a =
Test::Differences side-by-side comparison of the expected data structure =
and what we actually got. So it=E2=80=99s not like this is one big =
monolithic test that either succeeds or fails.

> An alternative approach would be to create the object, then write =
method calls which focus on individual attributes expected within that =
object:
>=20
> #####
> my $obj =3D $self->call( [
>         POST =3D> '/location/:location_id/build',
>         {
>             author     =3D> 'Wally Wallington',
>             structures =3D> [
>                 {
>                     type     =3D> 'dolmen',
>                     material =3D> 'concrete',
>                 }
>             ]
>         }
>     ]
> );
> is($obj->http_code, HTTP_CREATED, "Got expected HTTP response code");
> ok($obj->built, "'built' set to true value");
> ok(length($obj->author),
>  "I don't care what was set for 'author' as long as it's a =
non-zero-length string");
> #####

That=E2=80=99s exactly what I don=E2=80=99t want to do. Writing =
individual tests for each element of the data structure gets old very =
quickly, and it=E2=80=99s easy to decide you don=E2=80=99t want to do =
that and just skip some of the tests. (I=E2=80=99ve seen that happen in =
a project at $WORK which didn=E2=80=99t use this test framework, =
incidentally; this isn=E2=80=99t just supposition.)

The complicated expect structure is explicitly designed to be as concise =
and readable as possible, so e.g. you don=E2=80=99t have to write test =
titles for every comparison (semi-decent ones are generated for you), =
and your test can look like the data structure you=E2=80=99re getting =
back.

> One more specific objection:
>=20
> #####
> >     expect =3D> {
> ...
> >             build_id    =3D> qr{^ (?<build_id> BUILD \d+ ) $}x,
> ...
> >     }
> ...
> > # $self->state('build_id') got set by the named capture in the regex =
above.
> #####
>=20
> Is the 'build_id' something set by the process of creation of a new =
object and intrinsically part of that new object?  Or is it an artifact =
of testing that object?
>=20
> And if it is (as I suspect) generated in the creation of a new object, =
is its value predictable in advance?  If not predictable in advance -- =
if, say, it's partly composed of an epoch timestamp --  then you can't =
make any assertion about what its value ought to be.  That, in turn, =
means that it has no place in the 'expect' part of a unit test.

I=E2=80=99m not making an assertion about its exact value, though - =
I=E2=80=99m just saying =E2=80=9Cwe should get a build_id that looks =
like BUILD \d+=E2=80=9D, and if the resulting value doesn=E2=80=99t look =
like that, it=E2=80=99s a test failure. But if it does look reasonable, =
then that=E2=80=99s fine, and let=E2=80=99s remember it for later so we =
can e.g. test that subsequent calls return the exact same build ID.

> I would raise the same objection with respect to 'structure_id'.  I'd =
be much more inclined to make method calls on the new object and store =
the return values in variables for later use:
>=20
> #####
> my $build_id =3D $obj->build_id();
> my $structure_id =3D $obj->build_id();
> my $newobj =3D $self->call( [
>         PATCH =3D> =
"/location/$location_id/build/$build_id/structure/$structure_id",
>         { material =3D> 'gold' }
>     ]
> );
> #####

Again, that=E2=80=99s too cumbersome. Without the magic of named =
captures in a regex creating a state variable, I=E2=80=99d have to say:

my $data =3D $self->test(=E2=80=A6);
my $build_id =3D $data->{build_id};
my $structure_id =3D $data->{structures}[0]{structure_id};

which is (a) two and a bit extra lines of typing, and (b) potentially =
the source of an error.

But with the named capture magic, I can say in the same place =E2=80=9Cthi=
s value should look like this, and I want to remember it for later=E2=80=9D=
, no matter how deep in a data structure that value happens to be.

> Now, I concede that some of my objections are a matter of taste. =
TIMTOWTDI.  But I've written a lot of testing code over the years -- =
probably more testing code than "production" code -- and my gut feeling =
is that your testing apparatus is over-engineered for general use -- and =
perhaps even over-engineered for your own use.

I=E2=80=99ll happily admit that it=E2=80=99s not suitable for everything =
- it=E2=80=99s only really suitable for testing complicated data =
structures. But all of the complicated stuff - which has indeed built up =
over time, as you suspected - is there for a reason, and to make writing =
tests easier and nicer.

Sam
--=20
Website: http://www.illuminated.co.uk/