sebastianbergmann / php-code-coverage

Library that provides collection, processing, and rendering functionality for PHP code coverage information.
BSD 3-Clause "New" or "Revised" License
8.76k stars 370 forks source link

Improve coverage handling of value objects #1035

Closed gbirke closed 3 months ago

gbirke commented 3 months ago

With the advent of readonly classes in PHP 8.2, we're now able to have a lot of very simple value objects like this:

readonly class BookResult {
   public function construct(
      string $title,
      string $author,
      int $numberOfPages,
   ) {}
}

These value objects have no code that could be tested for behavior or functionality, but they still show up as uncovered in the coverage report, with the constructor being uncovered. I was wondering how we could improve this and would like to suggest some ideas:

The last two improvements would have the benefit of saving a lines of code, as we don't have to annotate every value object. The ValueObject attribute would make the intent very clear, without being too "magic".

What do you think?

sebastianbergmann commented 3 months ago

Adding a complete example for reference:

BookResult.php

<?php declare(strict_types=1);
final readonly class BookResult
{
   public function __construct(
      string $title,
      string $author,
      int $numberOfPages,
   ) {}
}

Code Coverage for BookResult.php when no BookResult object is created during tests

grafik

Code Coverage for BookResult.php when a BookResult object is created during tests

grafik

I used PHP 8.3.4, Xdebug 3.3.1, PHPUnit 11.0.8, and php-code-coverage 11.0.3.

sebastianbergmann commented 3 months ago

Constructor property promotion is used to avoid boilerplate code. But even if you do not need to write that code and do not see it when you look at the class, it is still there somehow. If no object of the class in question is created while the tests are running then the constructor of that class is correctly reported by the code coverage source, Xdebug in this case, to be executable and not executed.

I understand that the current behavior is annoying. If I would use constructor property promotion then it would annoy me for sure. But as I do not, I rely on others such as yourself to be annoyed and escalate that annoyance to me :-)

Jokes aside, I though that we were already "cleaning up" constructor property promotion and force line 8 in the above example to be not executable. Do I remember this wrong, @Slamdunk?

Slamdunk commented 3 months ago

You remember wrong :smile:

Many users rely on this very functionality to track down unused Value Objects: I would suggest to keep the current behaviour in php-code-coverage.

sebastianbergmann commented 3 months ago

I would suggest to keep the current behaviour in php-code-coverage.

I agree with you 😄, but unfortunately that does not help @gbirke.

sebastianbergmann commented 3 months ago
  • Should I just use the @codeCoverageIgnore annotation, with an explanation?

@gbirke I think that would be the best way to deal with this.

gbirke commented 3 months ago

You remember wrong 😄

Many users rely on this very functionality to track down unused Value Objects: I would suggest to keep the current behaviour in php-code-coverage.

What would be a typical setup for this use case? The way I'd set it up would be to add the coverage annotations/attributes to the test cases that use the value object (as described in my initial question). When the class and its test get deleted but not the value object, you get an uncovered class error in CI.

I can see additional the benefit of annotating your tests rather than your production code (with tool-specific annotations).

@sebastianbergmann An extreme example, just for my understanding: If I'd use CoversClass for my value object on every test class and then suddenly don't use the value object any more (neither in prod nor in test code) would it show up as covered or uncovered? Or would it show up at all?

@Slamdunk Do you have heuristics for where to put the annotation/attribute? One test class, all test classes, all test methods? Can you point me to repos where people do this, so I can see it in action (also maybe look at the CI setup)?

Slamdunk commented 3 months ago

If I'd use CoversClass for my value object on every test class and then suddenly don't use the value object any more (neither in prod nor in test code) would it show up as covered or uncovered? Or would it show up at all?

I would show up as uncovered

Do you have heuristics for where to put the annotation/attribute? One test class, all test classes, all test methods?

This choice is up to the developer. I prefer to put the #[CoversClass] only on tests that make sensible usage of that class, which in good projects should be just a few.

sebastianbergmann commented 3 months ago

@gbirke Can this be closed?