coding / php / tdd

October 25, 2018

How to Read and Improve the C.R.A.P Index of your code

I’m a big fan of TDD. I will try to put as many tests as I can on my projects. However, I’m still in learning phase. There are lots of things I still need to dig up in PHPUnit. While I’m at it, I discover something called as code coverage. Code coverage is a way check on how much the codes inside your project are being tested. Usually code coverage will give a report that contain lots of statistic about the code that being tested. One of the interesting metric for me in that report is C.R.A.P. Index.

What is C.R.A.P. Index ?

According to Alberto Savoia:

The C.R.A.P. (Change Risk Analysis and Predictions) index is designed to analyze and predict the amount of effort, pain, and time required to maintain an existing body of code.

Basically it’s a number designed to highlight potential problem areas in the code you’ve written. The higher the number, the crappier your code may be and the more likely it should be reexamined and refactored.

Testing This Theory – Write Some Crappy Code

Setting up files

Before we test that theory, we will need to set up our project. For this tutorial, i will use Docker to run the application – so you might need to install it on your machine. The project will contains these files and folders :

Project tree structure

│ .gitignore
│ composer.json
│ docker-compose.yml
│ Dockerfile
│ phpunit.xml
│ readme.md
│
├───app
└───tests

Files

/tests/coverage_results
/vendor

 

{
	"name":"hamzahjamad/phpunit-crap-index-tutorial",
	"description":"Tutorial about crap index",
	"license":"MIT",
	"type":"project",
	"require":{
		"php":"^7.1"
	},
	"require-dev":{
		"phpunit/phpunit": "^7.0"
	},
	"autoload": {
		"psr-4": {
			"App\\":"app/"
		}
	},
	"autoload-dev": {
        "psr-4": {
            "Tests\\": "tests/"
        }
    }
}

 

version: '2'
services:
  app:
    build:
      context: ./
      dockerfile: Dockerfile
    working_dir: /var/www
    volumes:
      - ./:/var/www

 

FROM php:7.2-fpm

RUN useradd me && pecl install xdebug \
    && docker-php-ext-enable xdebug

USER me 

 

<?xml version="1.0" encoding="UTF-8"?>
<phpunit 
	bootstrap="vendor/autoload.php"
	colors="true">
	<testsuite name="experiment">
		<directory>tests</directory>
	</testsuite>
	<filter>
		<whitelist processUncoveredFilesFromWhitelist="true">
			<directory suffix=".php">./app</directory>
		</whitelist>	
	</filter>	
</phpunit>	

 

Folders

  1. app
  2. tests

Installing packages

After setting up those files, run these commands to install all the packages needed for development.

# for linux environment
docker run --user $(id -u):$(id -g) --rm -v $(pwd):/app composer bash -c "composer install"

# for windows environment
# if you are using docker client, please enable it to access your drive
docker run --rm -v %cd%:/app composer bash -c "composer install"

Run the code

After installing the needed files, run this command to start the application.

docker-compose up

 

Write the code

Write a sample class to test – app/CrapClass.php

<?php
namespace App;

class CrapClass 
{
	public function listCities($state) {
		if ($state == "Sabah") {
			return "Kota Kinabalu, Sandakan, Tawau";
		} elseif ($state == "Sarawak") {
			return "Sibu, Kuching, Miri";
		} elseif ($state == "Selangor") {
			return "Sepang, Sabak Bernam, Gombak";
		} elseif ($state == "Johor") {
			return "Batu Pahat, Muar, Segamat";
		} elseif ($state == "Terengganu") {
			return "Dungun, Marang, Setiu";
		} else {
			throw new \UnexpectedValueException("Unknown state $state");
		}
	}	
}

Write a test class to test the sample class – tests/CrapClassTest.php

<?php
namespace Test;

use PHPUnit\Framework\TestCase;
use App\CrapClass;

class CrapClassTest extends TestCase
{

	protected $crapClass;
	
	protected function setUp() {
		$this->crapClass = new CrapClass;
	}

	protected function tearDown() {

	}

	public function testListCities(){
		$this->markTestIncomplete('This test has not been implemented yet');
	}
}

 

Run the test

After we done write the test and the class, run this command to run the test and generate the code coverage report.

docker-compose exec app bash -c "./vendor/bin/phpunit --coverage-html ./tests/coverage_results"

 

Checking up the first result

After PHPUnit done running the test, check the report inside tests/coverage_results folder and click on CrapClass.php.html file. You should see something like this.

As you can see, the CRAP index value is 42, which is pretty high. We can reduce it by completing our test.

Completing our test

Now reopen the tests/CrapClassTest.php file and update it with the code below.

<?php
namespace Test;

use PHPUnit\Framework\TestCase;
use App\CrapClass;

class CrapClassTest extends TestCase
{

	protected $crapClass;
	
	protected function setUp() {
		$this->crapClass = new CrapClass;
	}

	protected function tearDown() {

	}

	public function statesProvider() {
		return [
			["Sabah", "Kota Kinabalu, Sandakan, Tawau"],
			["Sarawak", "Sibu, Kuching, Miri"]
		];
	}

	/**
	* @dataProvider statesProvider
	*/
	public function testListCities($state, $result){
		$output = $this->crapClass->listCities($state);
		$this->assertEquals($result, $output);
	}
}

Rerun the tests

docker-compose exec app bash -c "./vendor/bin/phpunit --coverage-html ./tests/coverage_results"

 

Checking up the second test result


As you can see above, now the CRAP index is on 15.28. Plus, there are few lines not executed yet. We can reduce the CRAP index more by adding more data to test all the scenario.

 

Adding more scenario to our test

We can reduce the CRAP index by adding more scenario to our test. We can add the data and test for the Exception. Open our test, and update it like the code below.

<?php
namespace Test;

use PHPUnit\Framework\TestCase;
use App\CrapClass;

class CrapClassTest extends TestCase
{

	protected $crapClass;
	
	protected function setUp() {
		$this->crapClass = new CrapClass;
	}

	protected function tearDown() {

	}

	public function statesProvider() {
		return [
			["Sabah", "Kota Kinabalu, Sandakan, Tawau"],
			["Sarawak", "Sibu, Kuching, Miri"],
			["Selangor", "Sepang, Sabak Bernam, Gombak"],
		    ["Johor", "Batu Pahat, Muar, Segamat"],
		    ["Terengganu", "Dungun, Marang, Setiu"]
		];
	}

	/**
	* @dataProvider statesProvider
	*/
	public function testListCities($state, $result){
		$output = $this->crapClass->listCities($state);
		$this->assertEquals($result, $output);
	}

	/**
	* @expectedException UnexpectedValueException
	*/
	public function testListCitiesException() {
		$this->crapClass->listCities('Hiroshima');
	}
}

 

Rerun the tests

docker-compose exec app bash -c "./vendor/bin/phpunit --coverage-html ./tests/coverage_results"

 

Checking up the test for the third time


The value is 6 now, great! Our class also seems have 100% coverage which is pretty nice in this case ( 100% is not always a good number, sometimes there are class or file  didn’t need test – read more here ). But we can reduce it more, by refactor the class code.

Refactor the class

Now lets refactor our class to make it more clean. Open app/CrapClass.php file and update it with the code below.

<?php
namespace App;

class CrapClass 
{

    private $states;

    public function __construct() {
	    $this->states = [
			"Sabah" => ["Kota Kinabalu", "Sandakan", "Tawau"],
			"Sarawak" => ["Sibu", "Kuching", "Miri"],
			"Selangor" => ["Sepang", "Sabak Bernam", "Gombak"],
			"Johor" => ["Batu Pahat", "Muar", "Segamat"],
			"Terengganu" => ["Dungun", "Marang", "Setiu"],
		];
    }

	public function listCities($state) {
		if (!isset($this->states[$state])) {
		    throw new \UnexpectedValueException("Unknown state $state");
		} 

		return implode(", ", $this->states[$state]);
	}	
}

 

Rerun the tests

docker-compose exec app bash -c "./vendor/bin/phpunit --coverage-html ./tests/coverage_results"

 

Checking up the test result for the fourth time

Look at that! The value is 2 now ( for listCities method ), which is a nice number.  Seems like the code is more maintainable now too.

 

Final Thought

First of all, this post was inspired from this post. There are some changes on the source code example, but the idea is still the same. I also agree with the final thought part in that post, thus i will just copy it over here.

Final Thoughts

While lowering the C.R.A.P index of our code is always a good goal, it can easily create more problems than it solves. Refactoring your code just lower a number on a report is never a good idea. By arbitrarily lowering the C.R.A.P index, or any other programming metric for that matter, you not only go against the metric’s original intent, but you add an artificial and unnecessary complexity to your code that could end up doing more harm than good:

software metrics, in general, are just tools. No single metric can tell the whole story; it’s just one more data point. Metrics are meant to be used by developers, not the other way around – the metric should work for you, you should not have to work for the metric. Metrics should never be an end unto themselves. Metrics are meant to help you think, not to do the thinking for you. ~Alberto Savoia

Share

hamzahjamad

I write codes.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.