Publish Test Coverage to Codecov from Github Actions
Github Actions is one of ways to run Continues Integration. For Coverage report, we can use Codecov to publish the coverage result after running and generating test coverage.
For example, you have a Github Repository. You can open https://codecov.io/login and choose “Github”:
After you logged in, you can choose repository, or directly access https://codecov.io/gh/{your github user}/{your github repo}, for example, I use “samsonasik” as user, and “mezzio-authentication-with-authorization” as repository name:
https://codecov.io/gh/samsonasik/mezzio-authentication-with-authorization
On very first, we need to activate Webhook by open https://codecov.io/gh/gh/{your github user}/{your github repo}/settings, for example:
https://codecov.io/gh/samsonasik/mezzio-authentication-with-authorization/settings
Then, we click “Create new webhook” under Github Webhook:
After it done, we can copy “Repository Upload Token”:
by click “Copy” after then token, and back to Github, and save to Secrets section under Your Github Repository Settings with click “Add a new secret”, with eg: named: CODECOV_TOKEN, fill the value with your copied token, and click “Add secret” to save it to be like as follow:
The preparation is done. Now, time to add the github workflow, eg: “.github/workflows/ci-build.yml” at your repository, eg for php package/project and use phpunit, the workflow can be like the following:
name: "ci build" on: pull_request: push: branches: - "master" jobs: build: name: PHP ${{ matrix.php-versions }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: php-versions: ['7.2', '7.3', '7.4'] steps: - name: Setup PHP Action uses: shivammathur/setup-php@1.8.2 with: extensions: intl php-version: "${{ matrix.php-versions }}" coverage: pcov - name: Checkout uses: actions/checkout@v2 - name: "Validate composer.json and composer.lock" run: "composer validate" - name: "Install dependencies" run: "composer install --prefer-dist --no-progress --no-suggest && composer development-enable" - name: "Run test suite" run: "vendor/bin/phpunit --coverage-clover=coverage.xml" - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml flags: tests name: codecov-umbrella yml: ./codecov.yml fail_ci_if_error: true
That’s it. To trigger the Continues Integration, we can push a commit to the repository.
When its succeed (ci build green), we can then display coverage badge, eg at README.md like the following:
[](https://codecov.io/gh/{your github user}/{your github repo})
that will show:
Monkey Patch PHP Quit Statement with Patchwork
If your job is make tests for legacy app that has exit()
or die();
everywhere, and you don’t have privilege to refactor them, make tests for them may be hard as the test aborted when the quit statements executed in the tests. To test them, we need to redefine the user-defined functions and methods at runtime, and there is a lib for that, it is named Patchwork.
We can run command:
$ composer require antecedent/patchwork:*
to get the Patchwork dependency.
Let’s see how it can work, let say, we have a class:
namespace App; class MyClass { public function foo($arg) { if ($arg === 1) { return true; } exit('app exit.'); } }
Seeing the MyClass::foo
, we can only tests if the $arg
is equal then 1, otherwise, we need to redefine it, and there is a Patchwork\replace()
method for it by call like the following:
replace(MyClass::class. '::foo', function($arg) { if ($arg === 1) { pass(); } return 'app exit.'; });
The pass()
method will call original method functionality if $arg === 1
as that not return quit statement, otherwise we redefine to return string with value = ‘app exit’.
We can define in our unit test like this MyClassTest
class:
namespace AppTest; use PHPUnit_Framework_TestCase; use App\MyClass; use function Patchwork\pass; use function Patchwork\replace; use function Patchwork\undoAll; class MyClassTest extends PHPUnit_Framework_TestCase { private $myclass; protected function setUp() { replace(MyClass::class. '::foo', function($arg) { if ($arg === 1) { pass(); } return 'app exit.'; }); $this->myclass = new MyClass; } protected function tearDown() { undoAll(); } }
We can call the replace
it in setUp()
, and to undo, we can call undoAll()
in tearDown()
. And now, we can add the tests into MyClassTest
tests :
// ... public function provideFoo() { return [ [1, true], [0, 'app exit.'], ]; } /** * @dataProvider provideFoo */ public function testFoo($arg, $result) { $this->assertSame($result, $this->myclass->foo($arg)); } // ...
References to read:
– http://kahlan.readthedocs.org/en/latest/monkey-patching/
– http://antecedent.github.io/patchwork/
– http://afilina.com/testing-methods-that-make-static-calls/
Testing “expects($this->any())” with Prophecy with Spying
Testing method call from Collaborator that may be 0 or any other count with phpunit Framework test case can be done with expects($this->any())
from mock object. If we are using Prophecy for mocking tools, it can be done with spying
.
However, the spying itself need checks whenever method is called or not. We need findProphecyMethodCalls()
call againsts ObjectProphecy for that.
For example, we have a class with collaborator like the following:
namespace App; class Awesome { private $awesomeDependency; public function __construct(AwesomeDependency $awesomeDependency) { $this->awesomeDependency = $awesomeDependency; } public function process($data) { $rand = rand(1, 2); if ($rand === 1) { return $this->awesomeDependency->process($data); } return $data; } }
The rand()
usage is just a sample, in real app, we may have a heavy logic and it may fall to not call the collaborator.
The tests can be done like this:
namespace AppTest; use App\Awesome; use App\AwesomeDependency; use PHPUnit_Framework_TestCase; use Prophecy\Argument; use Prophecy\Argument\ArgumentsWildcard; class AwesomeTest extends PHPUnit_Framework_TestCase { protected function setUp() { $this->awesomeDependency = $this->prophesize(AwesomeDependency::class); $this->awesome = new Awesome($this->awesomeDependency->reveal()); } public function testProcess() { $data = [ 'foo' => 'abcdef', ]; // make \Prophecy\MethodProphecy instance $methodProphecy = $this->awesomeDependency ->process(Argument::exact($data)); // call method from actual instance $this->awesome->process($data); $calls = $this->awesomeDependency->findProphecyMethodCalls( 'process', new ArgumentsWildcard([$data]) ); $count = count($calls); // assert how many times it called $methodProphecy->shouldBeCalledTimes($count); if ($count) { // echoing just to prove echo 'Method from collaborator has been called'; $methodProphecy->shouldHaveBeenCalled(); } else { // echoing just to prove echo 'Method from collaborator has not been called'; $methodProphecy->shouldNotHaveBeenCalled(); } } }
Of course, it may be can’t be called ‘expecting’ before for something has done, it may be can be called as ‘recording’ what already happen, but by this usage, we can prove if it actually called in actual code.
leave a comment