Objektinio programavimo pagrindai ir gerosios praktikos. Testavimo pagrindai

Paysera Workshop

Šnekėsim apie OOP

  • Klasės ir objektai
  • Matomumo valdymas
  • Paveldėjimas
  • Interfeisai
  • Kompozicija
  • Functionalumo skaldymas

Pašnekėsim ir apie testavimą

  • Kas yra UnitTest
  • Kaip atrodo testai, kas yra assertations
  • Nepriklausymas nuo konteksto
  • Kas yra mock ir stub
  • Kaip rašyti testuojamą kodą

Kuo klasė skiriasi nuo objekto?

Klasė


class Mailer
{
    public function __construct(array $options)
    {
        // ...
    }

    public function sendEmail($email, $subject)
    {
        // ...
    }
}

Objektas


$mailer = new Mailer(['host' => 'localhost']);
$mailer->sendEmail('[email protected]', 'Hello!');

Klasė vietoj objekto


class Mailer
{
    public static function sendEmail($email, $subject)
    {
        // ...
    }
}
Mailer::sendEmail('[email protected]', 'Hello!');
					

Klasės laukai ir metodai


class Mailer
{
    private $host;
    private $port;
    // ...
    public function sendEmail($email, $subject)
    {
        $fullHost = $this->host . ':' . $this->port;
        // ...
    }
}
					

Laukų reikšmės - objektuose


$localMailer = new Mailer(['host' => 'localhost']);

$mailgun = new Mailer(['host' => 'mailgun.com']);

					

Inkapsuliacija

Valdom, kaip naudojamas mūsų kodas

Matomumas


class Mailer
{
    private $host;
    public function sendEmail($email, $subject)
    {
        $h = $this->buildHost();
        // ...
    }
    private function buildHost()
    {
        // ...
    }
}
					

Matomumas


$mailer = new Mailer([]);
$mailer->sendEmail('[email protected]', 'hello!');
$mailer->host = 'somehost.com'; // ERROR!
$mailer->buildHost(); // ERROR!
					

Ką tai leidžia?


class Mailer
{
    private $hostWithPort;  // rename
    public function sendEmail($email, $subject)
    {
        // ...
    }
    // remove, change behaviour, refactor
}
					

Vieši laukai


class Letter
{
    public $subject;
    public $body;
}
					

Kokios galimos problemos?

  • Sunku suvaldyti naudojimą (nustatomi laukų tipai ir pan.)
  • Negalim pakeisti functionalumo vienoje vietoje

Paveldėjimas

Klasės funkcionalumo praplėtimas


class LoggingMailer extends Mailer
{
    private $logger;
    public function setLogger($logger) { /* ... */ }
    public function sendEmailAndLog($email, $subject)
    {
        $this->sendEmail($email, $subject);
        $this->logger->info('Mail sent', [$email, $subject]);
    }
}
					

Naudojimas


$mailer = new LoggingMailer([]);
$mailer->setLogger($logger);

// same functionality
$mailer->sendEmail('[email protected]', 'hello!');
// new one
$mailer->sendEmailAndLog('[email protected]', 'hello!');
					

Typehints


function useMailer(Mailer $mailer) { /* ... */ }
function useExtendedMailer(LoggingMailer $mailer) { /* ... */ }

useMailer(new Mailer());
useMailer(new LoggingMailer());

useExtendedMailer(new LoggingMailer());
useExtendedMailer(new Mailer());    // ERROR!
					

Klasės funkcionalumo keitimas


class LoggingMailer extends Mailer
{
    private $logger;
    public function setLogger($logger) { /* ... */ }
    public function sendEmail($email, $subject)
    {
        parent::sendEmail($email, $subject);
        $this->logger->info('Mail sent', [$email, $subject]);
    }
}
					

Naudojimas


$mailer = new LoggingMailer([]);
$mailer->setLogger($logger);

// no need to change already written code:
$mailer->sendEmail('[email protected]', 'hello!');
					

Paveldėjimas per bet kiek lygių


class RepeatingMailer extends LoggingMailer
{
    public function sendEmail($email, $subject)
    {
        try {
            parent::sendEmail($email, $subject);
        } catch (\Exception $e) {
            parent::sendEmail($email, $subject);
        }
    }
}
					

Naudojimas


$mailer = new Mailer([]);

$mailer = new LoggingMailer([]);
$mailer->setLogger($logger);

$mailer = new RepeatingMailer([]);
$mailer->setLogger($logger);

// works differently with every mailer:
$mailer->sendEmail('[email protected]', 'hello!');
					

Laukų naudojimas iš paveldėtų/paveldinčių klasių


class RepeatingMailer extends LoggingMailer
{
    public function sendEmail($email, $subject)
    {
        try {
            parent::sendEmail($email, $subject);
        } catch (\Exception $e) {
            $this->logger->error($e); // ERROR!
            parent::sendEmail($email, $subject);
        }
    }
}
					

Matomumas tarp paveldinčių klasių


class LoggingMailer extends Mailer
{
    protected $logger;
    // ...
}
class RepeatingMailer extends LoggingMailer
{
    public function sendEmail($email, $subject)
    {
        // ...
        $this->logger->error($e); // OK to use
        // ...
    }
}
					

Liskov substitution principle

  • Kodas, naudojantis bazinės klasės objektą, turi veikti ir su paveldėtos klasės objektu
  • Paveldinti klasė turi tenkinti bazinės klasės kontraktą

$mailer->sendEmail($email, $subject);

class LoggingMailer extends Mailer
{
                                        // invalid:
    public function sendEmail($email, $subject, $body)
    {
        // ...
    }
}
					

$mailer->sendEmail($email, $subject);

class LoggingMailer extends Mailer
{
                                        // valid:
    public function sendEmail($myEmail, $subj, $body = null)
    {
        // ...
    }
}
					

class Letter {}
class ExtendedLetter extends Letter {}

$mailer->send(new Letter());

class LoggingMailer extends Mailer
{
                                        // invalid:
    public function send(ExtendedLetter $letter)
    {
        // ...
    }
}
					

Konstruktoriai gali skirtis!


$mailer = new Mailer([]);
$mailer = new LoggingMailer([], $logger);

// when using, code should not know which one it is
function useMailer(Mailer $mailer) {
    // ...
    $mailer->sendEmail($email, $subject);
}
					

Interfeisų naudojimas

Kas yra interfeisas?


interface MailerInterface
{
    public function sendEmail($email, $subject);
}
					

Kaip naudoti?


class Mailer implements MailerInterface
{
    public function sendEmail($email, $subject)
    {
        // ...
    }
}
class LoggingMailer implements MailerInterface { /* ... */ }

function useMailer(MailerInterface $mailer) { /* ... */ }
					

Kuo skiriasi nuo paveldėjimo?

  • Neturi kodo, tik kontraktą
    • Negalima sukonstruoti objekto
    • Neturi bazinio funkcionalumo
    • Naudojant mums neturi rūpėti funkcionalumas
  • Klasė gali įgyvendinti kelis interfeisus, paveldėti tik vieną klasę

Kada verta naudoti?

  • Jei yra kelios implementacijos

Kompozicija ir kodo skaldymas

Kas yra kompozicija?


class LoggingMailer extends Mailer
{
    private $logger;    // <- composition!
    public function setLogger($logger) { /* ... */ }
    public function sendEmail($email, $subject)
    {
        parent::sendEmail($email, $subject);
        $this->logger->info('Mail sent', [$email, $subject]);
    }
}
					

Kaip tai gali pakeisti paveldėjimą?


class LoggingMailer implements MailerInterface
{
    private $logger;
    private $mailer;
    public function __construct(
        MailerInterface $mailer,
        LoggerInterface $logger
    ) { /* ... */ }
    public function sendEmail($email, $subject)
    {
        $this->mailer->sendEmail($email, $subject);
        $this->logger->info('Mail sent', [$email, $subject]);
    }
}
					

Kaip tai gali pakeisti paveldėjimą?


class RepeatingMailer implements MailerInterface
{
    private $mailer;
    public function __construct(MailerInterface $mailer) { /**/ }
    public function sendEmail($email, $subject)
    {
        try {
            $this->mailer->sendEmail($email, $subject);
        } catch (Exception $e) {
            $this->mailer->sendEmail($email, $subject);
        }
    }
}
					

Kaip tai gali pakeisti paveldėjimą?


// send and log
$mailer = new LoggingMailer(new Mailer(), new Logger());
// send, if failed - repeat
$mailer = new RepeatingMailer(new Mailer());
// send, if failed - repeat. When sent - log
$mailer = new LoggingMailer(
    new RepeatingMailer(new Mailer()),
    new Logger()
);
// send and log, if failed - repeat
$mailer = new RepeatingMailer(
    new LoggingMailer(new Mailer(), new Logger())
);

function useMailer(MailerInterface $mailer) { /* any is ok */ }
					

Kam skaidom funkcionalumą?

  • Vienoje klasėje turim 3 metodus:
    • Duomenų nuskaitymas
    • Apdorojimas
    • Rezultatų išvedimas
  • Jei norime keisti bet kurį, reikia paveldėjimo
  • Jei turime 3 klases, galime naudoti kompoziciją
  • Kompozicija leidžia kiekvieną keisti, konfigūruoti, testuoti atskirai

Testavimas

Testų rūšys

  • Funkciniai testai
    • Rankiniai
    • Automatiniai
  • Unit testai - vienos klasės
  • Integraciniai testai - sistemos dalies

Kaip atrodo testas


class MailerTest extends TestCase
{
    public function testSendEmail()
    {
        $mailer = new Mailer([]);
        $this->assertTrue(
            $mailer->sendEmail('[email protected]', 'hello!')
        );
    }
    public function testSendEmailWithInvalidEmailAddress()
    {
        $mailer = new Mailer([]);
        $this->setExpectedException(MailerException::class);
        $mailer->sendEmail('not-an-email', 'hello!');
    }
}
					

Kokie yra assertations?

  • Dažniausiai naudojamas - assertEquals
  • assertSame - ===
  • assertCount, assertArrayHasKey, assertContains
  • assertFileExists, assertFileEquals
  • assert... - just use auto-complete if needed

Kontekstas ir priklausomybės

  • Paleidus mūsų testą - bus išsiųstas laiškas
  • Chatbot pilnam testavimui reikia facebook
  • Nėra Unit testas, jei priklausom nuo kito kodo:
    • Veiks lėčiau
    • Gali sulūžti dėl daug aplinkybių
    • Sunku patestuoti klasę su įvairiu veikimu

Mocks / stubs


class RepeatableMailerTest extends TestCase
{
    public function testSendEmail()
    {
        $innerMailerMock = $this->createMock(
            MailerInterface::class
        );
        $innerMailerMock
            ->expects($this->once())
            ->method('sendEmail')
            ->with('[email protected]', 'hello!')
        ;
        $mailer = new RepeatableMailer($innerMailerMock);
        $mailer->sendEmail('h[email protected]', 'hello!');
    }
					

Mocks / stubs


    public function testSendEmailWithFirstFailure()
    {
        $innerMailerMock = $this->createMock(MailerInterface::class);
        $innerMailerMock
            ->expects($this->at(0))
            ->method('sendEmail')
            ->will($this->throwException(new Exception()))
        ;
        $innerMailerMock
            ->expects($this->at(1))
            ->method('sendEmail')
            ->with('[email protected]', 'hello!')
        ;
        $mailer = new RepeatableMailer($innerMailerMock);
        $mailer->sendEmail('[email protected]', 'hello!');
    }
					

Mocks / stubs


    public function testSendEmailWithAllFailures()
    {
        $exception = new Exception();
        $innerMailerMock = $this->createMock(MailerInterface::class);
        $innerMailerMock
            ->expects($this->any())
            ->method('sendEmail')
            ->will($this->throwException($exception))
        ;
        $this->setExpectedException($exception);
        $mailer = new RepeatableMailer($innerMailerMock);
        $mailer->sendEmail('[email protected]', 'hello!');
    }
					

Mocks / stubs - kaip tai veikia?


$mock = $this->createMock(MailerInterface::class);
var_dump(get_class($mock));
// something like Mock_MailerInterface_7e1e0356
// always implements MockObject interface
// assertations registered in test case
					

Koks kodas nėra taip testuojamas?


class Mailer
{
    public function sendEmail($email, $subject)
    {
        mail($email, $subject, '...');
    }
}
					

Koks kodas nėra taip testuojamas?


class RepeatableMailer
{
    public function sendEmail($email, $subject)
    {
        $mailer = new Mailer([]);
        // ...
    }
}
					

Koks kodas nėra taip testuojamas?


Mailer::sendEmail('[email protected]', 'hello');
					

Koks kodas sunkiau testuojamas?


class RepeatableMailer extends Mailer
{
    public function sendEmail($email, $subject)
    {
        parent::sendEmail($email, $subject);
        // ...
    }
}
					

Koks kodas paprastai testuojamas?


class RepeatableMailer implements MailerInterface
{
    public function __construct(MailerInterface $mailer)
    {
        // ...
    }
    // ...
}
					

Tai vadinama priklausomybių įterpimu (dependency injection)

Koks kodas palaikomas?

Palaikomas kodas

  • Naudojam kompoziciją
  • Priklausomybes įterpiame
  • Skaldom funkcionalumą
  • Kiekvieną dalį gerai ištestuojam
  • Galim keisti veikimą pagal poreikius

Ką darysim šiandien?

Vakar dienos darbai

  • hello world
  • pasiimti klausimą per API
  • užduoti klausimą su quick reples
  • parašyti, ar teisingas atsakymas
  • skaičiuoti taškus

Šiandienos darbai

  • commit & push
  • išskaldome funkcionalumą klasėmis (PSR4)
  • klausimų "provider" imantis iš failo
  • klausimų "provider", imantis random iš failo arba API
  • testai kiekvienam iš klausimų "provider"
  • testas: ar užduodam gautą klausimą ir patikrinam atsakymą