Skip to content

Commit

Permalink
phpunit teardown validation
Browse files Browse the repository at this point in the history
- added teardown.pm and teared.pm modules to be able to test for
  test classes which do not unset their properties
  • Loading branch information
wickedOne committed Jul 13, 2024
1 parent e6d2e24 commit 26a4405
Show file tree
Hide file tree
Showing 8 changed files with 879 additions and 2 deletions.
4 changes: 2 additions & 2 deletions lib/GPH.pm
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package GPH;
use strict;
use warnings FATAL => 'all';

our $VERSION = '1.4.0';
our $VERSION = '1.4.1';

1;

Expand Down Expand Up @@ -55,7 +55,7 @@ generate custom configuration file for L<Psalm|https://psalm.dev/>
=head1 VERSION
1.4.0
1.4.1
=head1 AUTHOR
Expand Down
178 changes: 178 additions & 0 deletions lib/GPH/PHPUnit/Teardown.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package GPH::PHPUnit::Teardown;

use strict;
use warnings FATAL => 'all';

use GPH::PHPUnit::Teared;

sub new {
my ($proto, %args) = @_;

exists($args{files}) or die "file must be defined: $!";

my $self = bless {
files => $args{files},
debug => $args{debug} // 0,
strict => $args{strict} // 0,
teared => {},
}, $proto;

return ($self);
}

sub parse {
my ($self) = @_;
my ($fh);

foreach my $file (@{$self->{files}}) {
next unless $file =~ /Test|TestCase$/;

open($fh, '<', $file) or die "$!";

print "processing file: $file\n" unless $self->{debug} == 0;

my $teardown = 0;
my %properties = ();
my %teared = ();
my $in_teardown = 0;
my $seen_test = 0;

while (<$fh>) {
chomp $_;

# ignore comments and blank lines
next if $_ =~ /^[\s]{0,}[\/]{0,1}[\*]{1,2}/ or $_ eq '' or $_ =~ /^[\s]*\/\//;

# collect properties. strict mode uses all properties while in non strict mode non initialized & empty properties are used
my $pattern = $self->{strict} == 0
? '^[\s]*(?:private|public|protected)\s(?:static ){0,}([^\s]{0,})\s*\$([^\s;]+(?=;|\s=\s(?:\[\]|null)))'
: '^[\s]*(?:private|public|protected)\s(?:static ){0,}([^\s]{0,})\s*\$([^\s;]+)';

if ($seen_test == 0 && $_ =~ /$pattern/) {
$properties{$2} = $1;
print " property: $2 type: $1\n" unless $self->{debug} == 0;

next;
}

# assuming class properties are not defined all over the place
if ($_ =~ 'public function test') {
$seen_test = 1;
}

# check teardown methods
if ($_ =~ '([\s]+)(?:protected |public )function tearDown\(\): void'
or $_ =~ '([\s]+)(?:protected |public )static function tearDownAfterClass\(\): void'
) {
$teardown = 1;
$in_teardown = 1;
my $spaces = $1;

print " has teardown\n" unless $self->{debug} == 0;

while ($in_teardown == 1) {
my $line = <$fh>;
chomp $line;

my @matches = $line =~ /\$this->(\w+(?=(?:[ ,\)]|$)))/g;
my @statics = $line =~ /self::\$(\w+(?=(?:[ ,]|$)))/g;

foreach my $match (@matches, @statics) {
print " property: $match was found in teardown\n" unless $self->{debug} == 0;
$teared{$match} = 1;
}

if ($line =~ /$spaces}$/) {
$in_teardown = 0;
last;
}
}
}
}

close($fh);

$self->{teared}{$file} = GPH::PHPUnit::Teared->new((
file => $file,
teardown => $teardown,
properties => \%properties,
teared => \%teared,
));
}

return ($self);
};

sub validate {
my ($self) = @_;
my $exit = 0;

foreach my $teared (sort keys %{$self->{teared}}) {
if ($self->{teared}{$teared}->isValid() != 1 && $exit == 0) {
$exit = 1;
}
}

return ($exit);
};

1;

__END__
=head1 NAME
GPH::PHPUnit::Teardown - module to validate correct teardown behaviour of PHPUnit test classes.
see https://docs.phpunit.de/en/10.5/fixtures.html#more-setup-than-teardown for further information
=head1 SYNOPSIS
use GPH::PHPUnit::Teardown;
my $teardown = GPH::PHPUnit::Teardown->new((files => ['foo.php', 'bar.php'], debug => 1);
$teardown->parse();
=head1 METHODS
=over 4
=item C<< -E<gt>new(%args) >>
the C<new> method returns a GPH::PHPUnit::Teardown object. it takes a hash of options, valid option keys include:
=over
=item files B<(required)>
an array of file paths of files which you'd like to analyse.
=item debug
boolean whether or not to debug the parsing process.
=item strict
boolean whether or not to parse in strict mode (i.e. use all class properties regardless of initialisation state).
=back
=item C<< -E<gt>parse() >>
parse the files defined in C<< $self->{files} >>
=item C<< -E<gt>validate() >>
validate the parsed files. returns exit code 0 when all files are valid, 1 if one or more files are invalid
=back
=head1 AUTHOR
the GPH::PHPUnit::Teardown module was written by wicliff wolda <[email protected]>
=head1 COPYRIGHT AND LICENSE
this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut
111 changes: 111 additions & 0 deletions lib/GPH/PHPUnit/Teared.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package GPH::PHPUnit::Teared;

use strict;
use warnings FATAL => 'all';

sub new {
my ($proto, %args) = @_;

exists($args{file}) or die "file must be defined: $!";

my $self = bless {
file => $args{file},
teardown => $args{teardown} // 0,
properties => $args{properties} // {},
teared => $args{teared} // {},
}, $proto;

return ($self);
}

sub isValid {
my ($self) = @_;
my $properties = keys %{$self->{properties}};

if ($properties > 0 && $self->{teardown} == 0) {
print "file $self->{file} is invalid: has properties, but no teardown\n";

return(0);
}

my @missing = ();
foreach (keys %{$self->{properties}}) {
push (@missing, $_) unless exists($self->{teared}{$_});
}

@missing = sort @missing;

my $missing = @missing;

if ($missing > 0) {
print "file $self->{file} is invalid: propert" . ($missing == 1 ? "y '": "ies '") . join("', '", @missing) . "' " . ($missing == 1 ? "is ": "are ") . "not teared down\n";

return(0);
}

return(1);
};

1;

__END__
=head1 NAME
GPH::PHPUnit::Teared - data object for GPH::PHPUnit::Teardown module
=head1 SYNOPSIS
use GPH::PHPUnit::Teared;
my $teared = GPH::PHPUnit::Teared->new((file => 'foo.php', teardown => 1, properties => ('bar' => 1), teared => ('bar' => 1)));
$teared->isValid();
=head1 METHODS
=over 4
=item C<< -E<gt>new(%args) >>
the C<new> method returns a GPH::PHPUnit::Teared object. it takes a hash of options, valid option keys include:
=over
=item file B<(required)>
file (path) name used for validation output.
=item teardown
boolean whether or not the file contains a teardown method (can be tearDown or tearDownAfterClass).
=item properties
hash of class properties of the file
=item teared
hash of class properties which are 'touched' within a teardown method.
=back
=item C<< -E<gt>isValid() >>
validates the teardown behaviour of the file:
- if it has properties, a teardown method is required.
- if it has properties and one or more teardown methods, all properties need to be 'touched' within those methods.
returns 1 if valid, 0 otherwise.
=back
=head1 AUTHOR
the GPH::PHPUnit::Teared module was written by wicliff wolda <[email protected]>
=head1 COPYRIGHT AND LICENSE
this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut
29 changes: 29 additions & 0 deletions t/share/PHPUnit/InvalidTeardownTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Foo\Bar;

/**
* @author wicliff <[email protected]>
*/
class InvalidTeardownTestCase extends TestCase
{
private ?int $foo = null;
private static array $fixtures = [];
private $bar;

public function tearDown(): void
{
unset($this->foo);
}

public function testFooBar(): void
{
}

protected static function tearDownAfterClass(): void
{
self::$fixtures = [];
}
}
39 changes: 39 additions & 0 deletions t/share/PHPUnit/TeardownTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Foo\Bar;

/**
* @author wicliff <[email protected]>
*/
class TeardownTest extends TestCase
{
private ?int $foo = null;
private array $history = ['Foo', 'Bar'];
private string $bar = 'qux';
// comment
private array $fixtures = [];
private Configuration $config;
public static ?FooProvider $fooProvider;
public static BarProvider $barProvider;

public function tearDown(): void
{
unset($this->foo, $this->fixtures);

$this->config->reset();
}

public function testFooBar(): void
{
}

protected static function tearDownAfterClass(): void
{
self::$barProvider->reset();
self::$fooProvider = null;
}

private Processor $processor;
}
Loading

0 comments on commit 26a4405

Please sign in to comment.