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
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);
}
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
Testų rūšys
-
Funkciniai testai
- 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('[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 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)
Palaikomas kodas
- Naudojam kompoziciją
- Priklausomybes įterpiame
- Skaldom funkcionalumą
- Kiekvieną dalį gerai ištestuojam
- Galim keisti veikimą pagal poreikius
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ą