7

First of all, the title would suggest this is a duplicate of this or this, but for a couple of reasons, those answers don't work for me, even if my initial problem is the same. I'll explain why.

My problem is this: I have a few occasions in my code where I want to send a header and a body and then terminate processing. Unlike those other questions, I cannot use return or throw an Exception instead (those are obviously different functions designed for different purposes than exit, and this is not an error; it's simply an early runtime termination in some specific cases).

Still, I want to write unit tests that run these methods, make sure that the appropriate headers are set (solution found here), the output body is correct (solved with the $this->expectOutputString()-method in the test case) and then continue testing. In between, the exit will happen.

I've tried the @runInSeparateProcess-annotation in PHPUnit, I've also checked the test_helpers extension, which works, but I don't want to add another extension (be advised that tests will be run in production as well) for one single line of native PHP code that breaks everything. There must be a simpler way without sacrificing best practices.

Does anyone have a good solution to this problem?

Community
  • 1
  • 1

2 Answers2

4

I added a variable in the bootstrap that I could reference in the code with an IF in those rare occasions that I need to not exit.

define ('PHPUNIT_RUNNING', 1); 

Normal Program:

if(! @PHPUNIT_RUNNING === 1 )
{
    exit;
}

The PHPUnit is not defined as a rule, so there is a warning generated in PHP execution (which we hide with @. The code will then do what we want when not in test mode. This was added in after the main code was written as we added PHPUnit testing to an existing project, instead of doing TDD.

Please Note:

We do this as sparingly as possible to solve a legacy issue, otherwise we do as others have suggested and throw exceptions or return data to parent functions.

Steven Scott
  • 10,234
  • 9
  • 69
  • 117
  • Thanks for your reply, Steven. Yeah, my fallback solution is simply using a testing-environment variable to solve this issue, but I dislike having special code to make the tests work (`if(!Environment::isTesting()) { exit; }`). Currently, I'm using the [test_helpers](https://github.com/php-test-helpers/php-test-helpers) which allows me to run the specific method `set_exit_overload()` to basically overload exit with any function (i.e. empty). – Helge Talvik Söderström May 28 '14 at 18:20
  • I hated it as well, but unfortunately, I was not able to think of an easier way around this. The old code base we had was causing the problem of people taking the easy way out in the code and using Exit instead of trying to return or throw exceptions. More procedural code than true OO. – Steven Scott May 28 '14 at 19:34
2

I've just tested the following idea:

ExitException.php:

<?php
class DieException extends Exception {} // for those people who like die as well as exit
class ExitException extends Exception {}

entrypoint.php:

<?php
require_once 'ExitException.php';

try {
  require 'main_code.php';
  run_main_code();
}
catch (DieException $e) {
  return $e->getMessage();
}
catch (ExitException $e) {
  return $e->getMessage();
}

Several includes later:

deepcode.php

<?php
throw new ExitException('Exiting normally');

This simulates an exit or a die which is caught at the very top level. Your program will appear to exit as it did before and it is now testable without killing the entire test suite. The only gotcha is if you are catching other plain Exceptions in your existing code as well, in which case you will have to modify your code to rethrow the DieException/ExitException ones until they reach the top level.

The other alternative is to return cleanly which would probably involve rewriting a lot of your code. I.e., if you didn't exit at that point, why would your program generate further output? It should check as early as possible if an "early exit" is required, then just call that code and flow through to the natural end of the program.

This problem is made a lot harder if you have to deal with third-party libraries or other code that you shouldn't or can't change, but unless they come with their own unit tests, there's no value writing tests for them because ideally, you shouldn't be changing third party code to avoid future compatibility issues. A well-written third party library should never die. It should always return control to the calling program, or throw an Exception that can be caught.

CJ Dennis
  • 4,226
  • 2
  • 40
  • 69