-
-
Notifications
You must be signed in to change notification settings - Fork 149
Difficulty testing with exception design #53
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
Comments
I think catching whatever thrown inside Your first code seems like a mistake. The assert will do its normal job by fail and throw, but you aren't listening for any rejections. I think it's just a problem with how you are testing async stuff, none with #46. |
@daltones Okay, I understand what you mean about #46 as I was mostly reacting to the subject line. I was coming here to open this issue, and finding the subject line almost contradicted what I was coming here to suggest, it was disheartening. "Enforce \Exception instances as rejection values" when I want to suggest removing \Exception instances as rejection values. I'm not sure I understand your feedback on the first code. I think the first code is much cleaner and makes perfect sense compared with the second code block. I build a promise. The promise resolves successfully. Inside of the successful resolution, I want to assert that the successful resolution returned what is expected. Assert that the results are correct. PHPUnit throws an exception when the assert fails, but then PHPUnit doesn't actually fail the test because the exception is caught by the promise's then() try/catch block. The try/catch should be left to the developers using the promise library. If I expect an exception inside of the then function, I can The more I think about it, it really makes no sense to try/catch the resolve and push it over to a reject. |
@dustingraham You are thinking in a sync way. Seem's like you are not considering the real purpose of promises. Look at this sync code: try {
$result = someSyncOperation(); // will block and return when it's complete
} catch (\Exception $exception) {
echo "Error while doing Operation!";
$result = false;
}
// $result here sure will be the return value of someSyncOperation() or false on error.
// Just because it's sync! Now consider an async version like this: $result = null;
someAsyncOperation() // if it's a true async function, will return immediately.
->then(function ($value) use (&$result) {
$result = $value;
// $result here sure will be the return value of someAsyncOperation().
// But at this point could be a future time.
})
->catch(function (\Exception $exception) {
// Here the operation or what happened inside then() failed for sure.
});
// $result here probabily still will be null!
// Because everything above just returned immediately. When you try to write a test in a sync way with PHPUnit real results will be wasted and exceptions will be thrown to the limbo, just because your test method will return before. Your workaround in second code would only in a few cases, the ones where |
@dustingraham your third code is actually what you have to avoid when working with promises and async stuff. The point is on chaining |
You can use $promise
->then(function($results) {
$this->assertCount(2, $results);
})
->done();
But this does not really tackle the problem that PHPUnit isn't really suited for testing async code because it always assumes synchronous execution of the test methods. In JavaScript land, test frameworks offer tools for this. Pseudo code: test(function(done) {
expect(1); // Expect 1 assertion
asyncOperation()
.then(function(result) {
assert(result);
})
.then(done) // Notify that test is done
}); Additionally you can configure timeouts for tests, that means, if the test does not call Another option is, to use @clue's $rowcount = Block\await($promise, $loop);
$this->assertEquals(2, $rowcount); |
There may have been a misunderstanding. I'm not suggesting synchronous code for the asynchronous processing. Your second code block is what I agree with. However, one part of your statement which I disagree. // Here the operation or what happened inside then() failed for sure. The bolded part is what this entire issue is about. The current function names are As a developer, if I write some code that throws an exception, and I do not try/catch the exception. I should end up with some code that crashes. With the current promise implementation this is not true. Inside the
This should crash my program completely when I call Thanks! Adding My nested promise does something like this...
My original code did not have ->done() in there, so I tried adding it. However, the exception that occurs in the // handle result, such as a bad sql query or something, is caught by the try/catch around the resolve call. It bubbles the exception to reject, but then silently disappears. |
The last code block should throw the exception. Not sure why it does not, maybe a little bit more context is needed (the code block itself could be nested in another promise which swallows the rejection).
Not sure what you mean, i see no reject() in the code. |
@jsor
I dug into this a little more with a debugger. Here is where it appears the chain is dying. https://github.com/reactphp/promise/blob/master/src/Promise.php#L138 Perhaps the fix is if |
No, it won't crash throwing the exception and this is the expected behavior. Just look what you did: $promise = $deferred->promise();
$promise->then(function($result) {
throw new \Exception();
}); The second statement actually returns another promise. And this new one will reject, and then you can catch the exception thrown somewhere else. Imagine this: $promise = $deferred->promise();
$promise2 = $promise->then(function($result) {
throw new \Exception();
});
...
$promise2->otherwise(function (\Exception $reason) {
// Here's the thrown exception!
}); Maybe this is not what you actually want in your code, but to me is pretty right behavior. |
@daltones Thanks for your response. I added another response to jsor with a unit test that shows the problem. You're right about promise2 and the thrown exception. This makes sense and is logical. However, when promise 2, with the thrown exception, tries to resolve the nested Deferred, it silently dies because it thinks that it already resolved. Check out that test case. |
Btw I personally consider |
Here's what is happening: public function testSilentFail()
{
// Problem with nested promises.
$dQuery = new Deferred();
$dResult = new Deferred();
$dQuery->promise()
->then(function($result) use ($dResult) {
// Async Task Finished
$dResult->resolve('Success'); // <---- 2. BUG EXCEPTION bubbled here (OMG!)
})
->otherwise(function($problem) use ($dResult) {
// 3. BUG EXCEPTION was catched here as $problem
// But $dResult was already resolved above.
// Oops, seems there is a problem.
$dResult->reject($problem); // <--- THIS dies quietly.
// This is where the problem is. Reject immediately returns
// because $dResult promise already has a $this->result value.
})->done();
$dResult->promise()
->then(function($result) {
// Handle the result. Oops... programmer bug.
throw new \Exception('Programmer bug.'); // <---- 1. BUG EXCEPTION created and thrown here
})->done();
$this->setExpectedException('\Exception');
$dQuery->resolve('Async Query is Complete');
} As you can see, |
When I remove If a developer using this library creates a bug on the line above where the new exception is thrown, the promise library is currently quietly terminating in the above use case. |
First, promises are immutable. This means, once a promise is resolved (fulfilled or rejected), its state cannot be changed. I reordered your example code to better illustrate the flow: $dQuery = new Deferred();
$dResult = new Deferred();
$dQuery->promise()
->then(function($result) use ($dResult) {
$dResult->promise()
->then(function($result) {
// Handle the result. Oops... programmer bug.
throw new \Exception('Programmer bug.');
})->done();
$dResult->resolve('Success');
})
->otherwise(function($problem) use ($dResult) {
// $problem is `\Exception('Programmer bug.')` here!
// The following is a no-op because the promise of $dResult is already
// resolved in the previous then()!
$dResult->reject($problem);
// Because you do not "rethrow" $problem here, the rejection is
// considered as "catched".
// You have to `throw $problem` here or `return React\Promise\reject($problem)`.
})->done();
$this->setExpectedException('\Exception');
$dQuery->resolve('Async Query is Complete'); |
@jsor Ok... I see what you're saying. It is a little tricky because the This entire thread is a non-issue if only dealing with one or the other. But, when it can be either of those, and I must handle either a Since the entire point of this thread is that a developer can make a mistake in that success callback, I think what I'll do is create a I think I'm coming around to the idea here. The longer I stare at your re-written example, the more I feel like it makes sense that Appreciate your patience. |
You're welcome :) Feel free to post/link actual code here if you need help somewhere. |
Closing for now, but I may ping you depending on how implementation goes. Working on it now. Still wrapping my mind around the flow between the two layers of promises. Thanks. |
Contrary to #46 perhaps we shouldn't be catching \Exception in function then() at all.
Just spent a bit of time debugging why I couldn't unit test promises.
This was not failing the test when count was not correct. Finally figured out that phpunit is actually throwing an exception when the assert fails (I didn't know this), and promise->then() uses a try/catch around the resolve call (I also didn't know this.)
I feel like the try/catch from within the then() function probably makes things more difficult than it helps.
Granted, my workaround is to use(&$rowcount), but it still seems throwing an exception from within a resolve, ought to bounce out to the caller.
The text was updated successfully, but these errors were encountered: