New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove assertions (and helper methods) that operate on (non-public) attributes #3339
Comments
Interesting! When you have the time, can you share what the plans are? Should these be replaced by using non-specialized (or domain-specific) assertions in userland code? |
These assertions make it too easy to test implementation details ( |
Ha, I missed the |
Is there any related discussion that explain the thought behind this decision? |
A test should not depend on private implementation details. It was a bad idea to make a bad testing practice this convenient. |
I know that this is true but on the other hand this is just a misuse of a very helpful feature let me give you one of my use cases: namespace Whatever;
class ResetPasswordHandler
{
/** @var ResetPasswordRepository */
private $resetPasswordRepository;
public function __construct(ResetPasswordRepository $resetPasswordRepository)
{
$this->resetPasswordRepository = $resetPasswordRepository;
}
public function handle(string $oneTimeToken, string $email)
{
/** @var ResetPassword $resetPassword */
$resetPassword = $this->resetPasswordRepository->findOneBy(['email' => $email]);
if (!$resetPassword->hasReachedAttemptLimit() && $resetPassword->getToken() !== $oneTimeToken) {
$resetPassword->increaseAttempts();
$this->resetPasswordRepository->save($resetPassword);
return false;
} else {
return true;
}
}
}
/** My entity class */
class ResetPassword
{
private CONST MAX_ATTEMPTS = 3;
private $uuid;
private $attempts;
private $lastAttempt;
private $email;
private $token;
public function __construct($uuid, $email, $token, $attempts, \DateTimeInterface $lastAttempt)
{
$this->uuid = $uuid;
$this->attempts = $attempts;
$this->lastAttempt = $lastAttempt;
$this->email = $email;
$this->token = $token;
}
public function hasReachedAttemptLimit()
{
return $this->attempts >= self::MAX_ATTEMPTS;
}
public function increaseAttempts()
{
$this->attempts++;
$this->lastAttempt = new \DateTime();
}
/** @return string */
public function getToken()
{
return $this->token;
}
}
class ResetPasswordHandlerUnitTest extends TestCase
{
private $repository;
private $handler;
public function setUp()
{
$this->repository = $this->getMockBuilder(ResetPasswordHandler::class)->disableOriginalConstructor()->getMock();
$this->handler = new ResetPasswordHandler($this->repository);
}
public function testThatItIncreasedTheCount()
{
$entity = new ResetPassword('12234', 'test@test.com', 'whatevertoken', 0, new \DateTime());
$this->repository->expects($this->once())->method('findOneBy')->willReturn($entity);
$this->handler->handle('notmatch', 'test@test.com');
$this->assertAttributeSame(1, 'attempts', $entity);
}
} If you remove these functions in order for me to test that the developer has not made anything stupid like commenting a line like this:
I will need to create a getter function for attempts which honestly I hate doing. I mean you shouldn't add functions just in order to test your code. Another solution would be to extend this entity under a Test So again if a developer wants to actually test the private details of an object by removing this we are forcing them follow even worse paths. Note that I guess that most probably you will disagree with how the example is structured but similar scenarios happen regularly in everyday life and hence these are some pretty useful functions |
How about testing it like this instead? final class ResetPasswordTest extends TestCase
{
public function testHasReachedAttemptLimitReturnsFalseWhenAttemptLimitHasNotBeenReached(): void
{
$attempts = 0;
$resetPassword = new ResetPassword(
'12234',
'test@test.com',
'whatevertoken',
$attempts,
new \DateTimeImmutable()
);
$resetPassword->increaseAttempts();
$this->assertFalse($resetPassword->hasReachedAttemptLimit());
}
public function testHasReachedAttemptLimitReturnsTrueWhenAttemptLimitHasBeenReached(): void
{
$attempts = ResetPassword::MAX_ATTEMPTS - 1;
$resetPassword = new ResetPassword(
'12234',
'test@test.com',
'whatevertoken',
$attempts,
new \DateTimeImmutable()
);
$resetPassword->increaseAttempts();
$this->assertTrue($resetPassword->hasReachedAttemptLimit());
}
} There are always alternatives! 👍 |
Yea.. ok.. in this case you got me.. but the example was just for the sake of the argument... For instance what if he comments the There will be cases that this will make testing much harder driving developers to harder and worse paths... :( |
Hi @gmponos! Step away from the PHP for a minute. What behaviour do you want to test? How about this:
The testing problem is quickly solved by looking at the difference between testing implementation versus testing behaviour of a system. When I started working on the test execution reordering and its cache I ran into this question. The My first tests relied heavily on The cleanest solution turned out to be to only test the observable external behaviour:
|
The steps mentioned here do give a different perspective... 👍 Thanks for the explanation guys.. I will have them in mind and see how I can start removing these functions from my projects... and see how this goes.. Never the less at the moment as I see it as an overall I still insist on my 👎 hopefully I will be proven wrong at the future. |
@gmponos Hi, I'm working on instant migration tool @rectorphp. Would any automatization help you? E.g. use getter over private property? |
Unfortunatelly no.. IMHO open an issue if you wish at your repo and I will explain you my arguments there... no need to start a discussion about your tool here and add extra noise :) |
I think this is the best place for upgrade paths most people would look for. If there is none, no troubes :) |
The upgrade path is to refactor your (test) code to not require the assertions in question. |
@TomasVotruba look.. most probably in the places that I used one of these assertions I did not had a if you create a tool just for the cases that the |
@gmponos I just wasn't sure how people use it, since I have no experience with that. It's clear to me now there is no way to automate this. I look for A -> B changes |
This means we have no way to test private properties after this PR ?? |
If you see no way around inspecting private properties you can still use reflection. |
As far as I know PHPUnit uses reflection in order to assert these properties with these functions. So in the end the developers instead of using the wrapper of PHPUnit will end to do reflection on their own... Also I have been thinking about your comment before...
Yes this is true... but I am trying to do unit test... I would accept this argument from a library that does behavior testing like behat... not from PHPUnit.. which is as the name suggests for Unit tests... |
I see what you mean. I think unit testing doesn't mean to test all private properties/metods, but rather public api of the object. If we have big class with 50 private methdos, we shouldn't use reflection to test private elements. We should decouple to standalone responsible objects. And then, test their public api. |
@TomasVotruba I was not talking about testing actual private methods... but about testing public methods that do some alteration... Check the following example for instance:
my only way to test this now is
While before I was:
I do agree that first way may seem more "sophisticated" but isn't this getting out of the scope of actual unit testing and moving to another level or is it just me thinking this way? Edit also the same thing applies for my previous example.. Yes I can test it with the way that it is mentioned.. executing the test with four attempts etc etc... but again this reminds mostly practices from testing suites related with behavior... |
Well yes and water is wet. If you cannot implicitly use PHPUnit's reflection-based wrapper, you will have to write your own. Which I am sure some people will do and even create a nice thing on Packagist for it.
How does a 'unit' test not test the behaviour of the system under test? If you want get creative with the language surrounding testing, sure! Here is one: we cannot even unit test, it is impossible. We are always testing integration: your code with PHP, a filesystem, an OS (with multitasking and IO interrupts, no less!) Your tests lock down the implementation of a thing by inspecting the inside. That is not 'wrong' as such; sometimes you have to do that, mostly when it comes to licensing/auditing/legal reasons. You want to make sure people do Some references from current PHPUnit development work:
Hope this provides you with a bit of a scratching pole for your thoughts about testing. Wanting to look inside is not wrong but it has a shitload of side-effects. |
Thank you for providing so much detail on your thought process and example code. That's really great. :) The logger example is a classic and you could tests this a bit 'quicker' by using dependency injection (e.g. a "log output stream thingy" that you can check the buffer of) or using a Mock with expectations on the calls. Which is turn out to be very much alike once you draw it out on paper. |
Based on the changelog of PHPUnit 9.0, support for these constraints has been dropped. Refs: * https://github.com/sebastianbergmann/phpunit/blob/9.0.1/ChangeLog-9.0.md * sebastianbergmann/phpunit#3339
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
…ls in constructor tests I removed the tests completely because they just test that the constructor assigns the values to the internal properties. Nothing that should be cared about from the outside. See sebastianbergmann/phpunit#3339 (comment) It is seen as bad practice to test internal stuff of objects instead of the actual input and output of mathod calls. Signed-off-by: Morris Jobke <hey@morrisjobke.de>
Signed-off-by: andyo <javabudd@gmail.com>
Wrong way, let people to test everything ( |
The following methods should be removed:
assertAttributeContains()
assertAttributeNotContains()
assertAttributeContainsOnly()
assertAttributeNotContainsOnly()
assertAttributeCount()
assertAttributeNotCount()
assertAttributeEquals()
assertAttributeNotEquals()
assertAttributeEmpty()
assertAttributeNotEmpty()
assertAttributeGreaterThan()
assertAttributeGreaterThanOrEqual()
assertAttributeLessThan()
assertAttributeLessThanOrEqual()
assertAttributeSame()
assertAttributeNotSame()
assertAttributeInstanceOf()
assertAttributeNotInstanceOf()
assertAttributeInternalType()
assertAttributeNotInternalType()
attribute()
attributeEqualTo()
readAttribute()
getStaticAttribute()
getObjectAttribute
The text was updated successfully, but these errors were encountered: