Skip to content

Commit 2a3416b

Browse files
committed
phpunit teardown validation
- added teardown.pm and teared.pm modules to be able to test for test classes which do not unset their properties
1 parent e6d2e24 commit 2a3416b

File tree

8 files changed

+879
-2
lines changed

8 files changed

+879
-2
lines changed

lib/GPH.pm

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package GPH;
33
use strict;
44
use warnings FATAL => 'all';
55

6-
our $VERSION = '1.4.0';
6+
our $VERSION = '1.4.1';
77

88
1;
99

@@ -55,7 +55,7 @@ generate custom configuration file for L<Psalm|https://psalm.dev/>
5555
5656
=head1 VERSION
5757
58-
1.4.0
58+
1.4.1
5959
6060
=head1 AUTHOR
6161

lib/GPH/PHPUnit/Teardown.pm

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package GPH::PHPUnit::Teardown;
2+
3+
use strict;
4+
use warnings FATAL => 'all';
5+
6+
use GPH::PHPUnit::Teared;
7+
8+
sub new {
9+
my ($proto, %args) = @_;
10+
11+
exists($args{files}) or die "file must be defined: $!";
12+
13+
my $self = bless {
14+
files => $args{files},
15+
debug => $args{debug} // 0,
16+
strict => $args{strict} // 0,
17+
teared => {},
18+
}, $proto;
19+
20+
return ($self);
21+
}
22+
23+
sub parse {
24+
my ($self) = @_;
25+
my ($fh);
26+
27+
foreach my $file (@{$self->{files}}) {
28+
next unless $file =~ /Test|TestCase$/;
29+
30+
open($fh, '<', $file) or die "$!";
31+
32+
print "processing file: $file\n" unless $self->{debug} == 0;
33+
34+
my $teardown = 0;
35+
my %properties = ();
36+
my %teared = ();
37+
my $in_teardown = 0;
38+
my $seen_test = 0;
39+
40+
while (<$fh>) {
41+
chomp $_;
42+
43+
# ignore comments and blank lines
44+
next if $_ =~ /^[\s]{0,}[\/]{0,1}[\*]{1,2}/ or $_ eq '' or $_ =~ /^[\s]*\/\//;
45+
46+
# collect properties. strict mode uses all properties while in non strict mode non initialized & empty properties are used
47+
my $pattern = $self->{strict} == 0
48+
? '^[\s]*(?:private|public|protected)\s(?:static ){0,}([^\s]{0,})\s*\$([^\s;]+(?=;|\s=\s(?:\[\]|null)))'
49+
: '^[\s]*(?:private|public|protected)\s(?:static ){0,}([^\s]{0,})\s*\$([^\s;]+)';
50+
51+
if ($seen_test == 0 && $_ =~ /$pattern/) {
52+
$properties{$2} = $1;
53+
print " property: $2 type: $1\n" unless $self->{debug} == 0;
54+
55+
next;
56+
}
57+
58+
# assuming class properties are not defined all over the place
59+
if ($_ =~ 'public function test') {
60+
$seen_test = 1;
61+
}
62+
63+
# check teardown methods
64+
if ($_ =~ '([\s]+)(?:protected |public )function tearDown\(\): void'
65+
or $_ =~ '([\s]+)(?:protected |public )static function tearDownAfterClass\(\): void'
66+
) {
67+
$teardown = 1;
68+
$in_teardown = 1;
69+
my $spaces = $1;
70+
71+
print " has teardown\n" unless $self->{debug} == 0;
72+
73+
while ($in_teardown == 1) {
74+
my $line = <$fh>;
75+
chomp $line;
76+
77+
my @matches = $line =~ /\$this->(\w+(?=(?:[ ,\)]|$)))/g;
78+
my @statics = $line =~ /self::\$(\w+(?=(?:[ ,]|$)))/g;
79+
80+
foreach my $match (@matches, @statics) {
81+
print " property: $match was found in teardown\n" unless $self->{debug} == 0;
82+
$teared{$match} = 1;
83+
}
84+
85+
if ($line =~ /$spaces}$/) {
86+
$in_teardown = 0;
87+
last;
88+
}
89+
}
90+
}
91+
}
92+
93+
close($fh);
94+
95+
$self->{teared}{$file} = GPH::PHPUnit::Teared->new((
96+
file => $file,
97+
teardown => $teardown,
98+
properties => \%properties,
99+
teared => \%teared,
100+
));
101+
}
102+
103+
return ($self);
104+
};
105+
106+
sub validate {
107+
my ($self) = @_;
108+
my $exit = 0;
109+
110+
foreach my $teared (sort keys %{$self->{teared}}) {
111+
if ($self->{teared}{$teared}->isValid() != 1 && $exit == 0) {
112+
$exit = 1;
113+
}
114+
}
115+
116+
return ($exit);
117+
};
118+
119+
1;
120+
121+
__END__
122+
123+
=head1 NAME
124+
125+
GPH::PHPUnit::Teardown - module to validate correct teardown behaviour of PHPUnit test classes.
126+
127+
see https://docs.phpunit.de/en/10.5/fixtures.html#more-setup-than-teardown for further information
128+
129+
=head1 SYNOPSIS
130+
131+
use GPH::PHPUnit::Teardown;
132+
133+
my $teardown = GPH::PHPUnit::Teardown->new((files => ['foo.php', 'bar.php'], debug => 1);
134+
$teardown->parse();
135+
136+
=head1 METHODS
137+
138+
=over 4
139+
140+
=item C<< -E<gt>new(%args) >>
141+
142+
the C<new> method returns a GPH::PHPUnit::Teardown object. it takes a hash of options, valid option keys include:
143+
144+
=over
145+
146+
=item files B<(required)>
147+
148+
an array of file paths of files which you'd like to analyse.
149+
150+
=item debug
151+
152+
boolean whether or not to debug the parsing process.
153+
154+
=item strict
155+
156+
boolean whether or not to parse in strict mode (i.e. use all class properties regardless of initialisation state).
157+
158+
=back
159+
160+
=item C<< -E<gt>parse() >>
161+
162+
parse the files defined in C<< $self->{files} >>
163+
164+
=item C<< -E<gt>validate() >>
165+
166+
validate the parsed files. returns exit code 0 when all files are valid, 1 if one or more files are invalid
167+
168+
=back
169+
170+
=head1 AUTHOR
171+
172+
the GPH::PHPUnit::Teardown module was written by wicliff wolda <[email protected]>
173+
174+
=head1 COPYRIGHT AND LICENSE
175+
176+
this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
177+
178+
=cut

lib/GPH/PHPUnit/Teared.pm

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package GPH::PHPUnit::Teared;
2+
3+
use strict;
4+
use warnings FATAL => 'all';
5+
6+
sub new {
7+
my ($proto, %args) = @_;
8+
9+
exists($args{file}) or die "file must be defined: $!";
10+
11+
my $self = bless {
12+
file => $args{file},
13+
teardown => $args{teardown} // 0,
14+
properties => $args{properties} // {},
15+
teared => $args{teared} // {},
16+
}, $proto;
17+
18+
return ($self);
19+
}
20+
21+
sub isValid {
22+
my ($self) = @_;
23+
my $properties = keys %{$self->{properties}};
24+
25+
if ($properties > 0 && $self->{teardown} == 0) {
26+
print "file $self->{file} is invalid: has properties, but no teardown\n";
27+
28+
return(0);
29+
}
30+
31+
my @missing = ();
32+
foreach (keys %{$self->{properties}}) {
33+
push (@missing, $_) unless exists($self->{teared}{$_});
34+
}
35+
36+
@missing = sort @missing;
37+
38+
my $missing = @missing;
39+
40+
if ($missing > 0) {
41+
print "file $self->{file} is invalid: propert" . ($missing == 1 ? "y '": "ies '") . join("', '", @missing) . "' " . ($missing == 1 ? "is ": "are ") . "not teared down\n";
42+
43+
return(0);
44+
}
45+
46+
return(1);
47+
};
48+
49+
1;
50+
51+
__END__
52+
53+
=head1 NAME
54+
55+
GPH::PHPUnit::Teared - data object for GPH::PHPUnit::Teardown module
56+
57+
=head1 SYNOPSIS
58+
59+
use GPH::PHPUnit::Teared;
60+
61+
my $teared = GPH::PHPUnit::Teared->new((file => 'foo.php', teardown => 1, properties => ('bar' => 1), teared => ('bar' => 1)));
62+
$teared->isValid();
63+
64+
=head1 METHODS
65+
66+
=over 4
67+
68+
=item C<< -E<gt>new(%args) >>
69+
70+
the C<new> method returns a GPH::PHPUnit::Teared object. it takes a hash of options, valid option keys include:
71+
72+
=over
73+
74+
=item file B<(required)>
75+
76+
file (path) name used for validation output.
77+
78+
=item teardown
79+
80+
boolean whether or not the file contains a teardown method (can be tearDown or tearDownAfterClass).
81+
82+
=item properties
83+
84+
hash of class properties of the file
85+
86+
=item teared
87+
88+
hash of class properties which are 'touched' within a teardown method.
89+
90+
=back
91+
92+
=item C<< -E<gt>isValid() >>
93+
94+
validates the teardown behaviour of the file:
95+
96+
- if it has properties, a teardown method is required.
97+
- if it has properties and one or more teardown methods, all properties need to be 'touched' within those methods.
98+
99+
returns 1 if valid, 0 otherwise.
100+
101+
=back
102+
103+
=head1 AUTHOR
104+
105+
the GPH::PHPUnit::Teared module was written by wicliff wolda <[email protected]>
106+
107+
=head1 COPYRIGHT AND LICENSE
108+
109+
this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
110+
111+
=cut
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Foo\Bar;
6+
7+
/**
8+
* @author wicliff <[email protected]>
9+
*/
10+
class InvalidTeardownTestCase extends TestCase
11+
{
12+
private ?int $foo = null;
13+
private static array $fixtures = [];
14+
private $bar;
15+
16+
public function tearDown(): void
17+
{
18+
unset($this->foo);
19+
}
20+
21+
public function testFooBar(): void
22+
{
23+
}
24+
25+
protected static function tearDownAfterClass(): void
26+
{
27+
self::$fixtures = [];
28+
}
29+
}

t/share/PHPUnit/TeardownTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Foo\Bar;
6+
7+
/**
8+
* @author wicliff <[email protected]>
9+
*/
10+
class TeardownTest extends TestCase
11+
{
12+
private ?int $foo = null;
13+
private array $history = ['Foo', 'Bar'];
14+
private string $bar = 'qux';
15+
// comment
16+
private array $fixtures = [];
17+
private Configuration $config;
18+
public static ?FooProvider $fooProvider;
19+
public static BarProvider $barProvider;
20+
21+
public function tearDown(): void
22+
{
23+
unset($this->foo, $this->fixtures);
24+
25+
$this->config->reset();
26+
}
27+
28+
public function testFooBar(): void
29+
{
30+
}
31+
32+
protected static function tearDownAfterClass(): void
33+
{
34+
self::$barProvider->reset();
35+
self::$fooProvider = null;
36+
}
37+
38+
private Processor $processor;
39+
}

0 commit comments

Comments
 (0)