Testing - philosophy of a modern developer

TDD

BDD

ATDD

Unit Tests

Functional  Tests

Acceptance Tests

White Box vs Black Box 

Unit Tests = AAA

  • Arrange

  • Act

  • Assert

Example

public class СalculatorTests {
    @Test
    public void multiplicationOfZeroIntegersShouldReturnZero() {
        // Calculator, being tested
        Calculator сalculator = new Calculator();
        // asserts
        assertEquals(
                0,
                сalculator.multiply(10, 0),
                "10 x 0 must be 0"
        );
        assertEquals(
                0,
                сalculator.multiply(0, 10),
                 "0 x 10 must be 0"
        );
    }
}

Functional Tests = GWT

  • Given - ititial app state

  • When - actions of a user

  • Then - result

Example 

<?php 
$I->amOnPage('/'); 
      
$I->click('Login'); 
     
$I->fillField('Username', 'Miles');    
$I->fillField('Password', 'Davis');  
 
$I->click('Enter');  
      
$I->see('Hello, Miles', 'h1');  

Acceptance Tests 

Feature: Basic Arithmetic

  Background: A Calculator
    Given a calculator, I just turned on

  Scenario: Addition
    When I add 4 and 5
    Then the result is 9

  Scenario: Another Addition
    When I add 4 and 7
    Then the result is 11

Integration Tests 

  • Acceptance

  • Functional

  • Unit

ATDD
BDD
TDD

=

Methodology

TDD

Test fails

Test succeed

Refactoring

BDD

Specification

Development

Communication

ATDD

Test Development

Development

Specification

BDD vs ATDD

T is the key

Acceptance

Functional

Integration

Unit

Problems?

What problems?

Not To Write Tests

We can't sell

them!!!!

To Write Tests

public class UserTests extends ActiveRecord {
    String name;
    String lastName;
}

public class UserTests {
    @Test
    public void saveUser() {
        User user = new User();
        
        user.name = "John";
        user.lastName = "Dow";
        

        assertTrue(
                user.save(),
                "user must be saved"
        );
    }
}

Pseudocode

Example From Yii2 Source Code


    public function testConstructor()
    {
        $config = [
            'on' => ['a' => 'b'],
            'joinWith' => ['dummy relation'],
        ];
        $query = new ActiveQuery(Customer::className(), $config);
        $this->assertEquals($query->modelClass, Customer::className());
        $this->assertEquals($query->on, $config['on']);
        $this->assertEquals($query->joinWith, $config['joinWith']);
    }
public class User extends ActiveRecord {
    String name;
    String lastName;
    Integer status;

    
    public boolean isActive() {
        return status == 1;
    }
}

public class UserTests {
    @Test
    public void saveUser() {
        User user = User.findOne(1);    

        assertTrue(
                user.isActive(),
                "user must be active"
        );
    }
}

To Write Redundant Tests

Write Just "For The Record"

Chief

Job done I!


    public function testDo() {
        $model = new User();

        $this->assertTrue($model->do());

        $this->assertFalse($model->do());
    }

Pseudocode

Write Tests "For Yourself"

Lack Of Informativity


    public function testSaveLoad()
    {
        static::$time = static::$filemtime = \time();
        $this->prepareData();
        $items = $this->auth->items;
        $children = $this->auth->children;
        $assignments = $this->auth->assignments;
        $rules = $this->auth->rules;
        $this->auth->save();
        $this->auth = $this->createManager();
        $this->auth->load();
        $this->assertEquals($items, $this->auth->items);
        $this->assertEquals($children, $this->auth->children);
        $this->assertEquals($assignments, $this->auth->assignments);
        $this->assertEquals($rules, $this->auth->rules);
    }

Yii2 Rocks

    public function testInitControllerNamePluralization() {
        $suites = $this->getTestsForControllerNamePluralization();
        foreach ($suites as $i => $suite) {
            list($name, $tests) = $suite;
            foreach ($tests as $j => $test) {
                list($config, $expected) = $test;
                $rule = new UrlRule($config);
                $this->assertEquals($expected, $rule->controller, "Test#$i-$j: $name");
            }
        }
    }

    public function testRenderWithCustomInputId() {
        $expectedValue = <<<EOD
<div class="form-group field-custom-input-id">
<label class="control-label" for="custom-input-id">Attribute Name</label>
<input type="text" id="custom-input-id" class="form-control" name="ActiveFieldTestModel[{$this->attributeName}]">
<div class="hint-block">Hint for attributeName attribute</div>
<div class="help-block"></div>
</div>
EOD;
        $this->activeField->inputOptions['id'] = 'custom-input-id';
        $actualValue = $this->activeField->render();
        $this->assertEqualsWithoutLE($expectedValue, $actualValue);
    }

Error Messages

Would Make Us Happy

Magic Numbers

    public function testFindScalar()
    {
        /* @var $customerClass \yii\db\ActiveRecordInterface */
        $customerClass = $this->getCustomerClass();
        /* @var $this TestCase|ActiveRecordTestTrait */
        // query scalar
        $customerName = $customerClass::find()->where(['id' => 2])->scalar('name');
        $this->assertEquals('user2', $customerName);

        $customerName = $customerClass::find()->where(['status' => 2])->scalar('name');
        $this->assertEquals('user3', $customerName);

        $customerName = $customerClass::find()->where(['status' => 2])->scalar('noname');
        $this->assertNull($customerName);

        $customerId = $customerClass::find()->where(['status' => 2])->scalar('id');
        $this->assertEquals(3, $customerId);
    }

Code Specs

Saving The Day

class IncomeCalculatorTest extends Specification {
    private const EXPECTED_TAX_FOR_FIRST_LEVEL_TAX_RULE = 4500;
    private const EXPECTED_TAX_FOR_SECOND_LEVEL_TAX_RULE = 7200;
    private const EXPECTED_TAX_FOR_THIRD_LEVEL_TAX_RULE = 30000;
    private const INCOME_AFTER_APPLYING_FIRST_LEVEL_TAX_RULE = 300000;

    /**
     * @test
     */
    public function calculateTaxSpec() {
        $clientsPayments = []; // dummy variable, just for example
        $hoursSpentWorking = 160; // dummy variable, just for example
        $service = new IncomeCalculator($clientsPayments, $hoursSpentWorking);

        $I = $this->tester;
        $I->describe('income tax calculations');

        $I->expectThat('for income less that 50 000 calculator use 10% tax rule');
        
        $I->lookAt('first level income tax');
        $I->seeNumber($service->calculateTax())
          ->isNotEmpty()
          ->isEqualTo(self::EXPECTED_TAX_FOR_FIRST_LEVEL_TAX_RULE);
    }
}
    /**
     * @test
     */
    public function calculateWithTaxSpec() {
        $clientsPayments = []; // dummy variable, just for example
        $hoursSpentWorking = 160; // dummy variable, just for example
        $service = new IncomeCalculator($clientsPayments, $hoursSpentWorking);

        $I = $this->tester;
        $I->describe('income calculation');

        $I->lookAt('income tax');

        $I->expectThat(
             'calculator calculates income with tax 
             using 10% tax rule 
             for income less that 50 000'
        );

        $I->seeNumber($service->calculateWithTax())
            ->isNotEmpty()
            ->isEqualTo(self::INCOME_AFTER_APPLYING_FIRST_LEVEL_TAX_RULE);
    }

Error Report Informativity

Scenario And Step Description

$I->describe('income tax calculations');

$I->expectThat('for income less that 50 000 calculator use 10% tax rule');

$I->expectThat('for income between 50 000 and 100 000 calculator use 12% tax rule');

$I->expectThat('for income more than 100 000 calculator use 20% tax rule');

Constants For Magic Numbers

private const EXPECTED_TAX_FOR_FIRST_LEVEL_TAX_RULE = 4500;
private const EXPECTED_TAX_FOR_SECOND_LEVEL_TAX_RULE = 7200;
private const EXPECTED_TAX_FOR_THIRD_LEVEL_TAX_RULE = 30000;
private const INCOME_AFTER_APPLYING_FIRST_LEVEL_TAX_RULE = 300000;

.................


->isEqualTo(self::EXPECTED_TAX_FOR_FIRST_LEVEL_TAX_RULE);

->isEqualTo(self::EXPECTED_TAX_FOR_SECOND_LEVEL_TAX_RULE);

->isEqualTo(self::EXPECTED_TAX_FOR_THIRD_LEVEL_TAX_RULE);

->isEqualTo(self::INCOME_AFTER_APPLYING_FIRST_LEVEL_TAX_RULE);

DSL

$I->lookAt('third level income tax');
$I->seeNumber('income tax', $service->calculateTax())
  ->isNotEmpty()
  ->isEqualTo(self::EXPECTED_TAX_FOR_THIRD_LEVEL_TAX_RULE);


$I->seeBool(true)
  ->isFalse();

$I->lookAt('user fetched from DB');
$I->seeArray(['name' => 'John'])
  ->isNotEmpty()
  ->hasKey('name')
  ->hasKey('lastName');

Questions?

Testing - philosophy of a modern developer

By Dima Kolodko

Testing - philosophy of a modern developer

  • 489