Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Jakość dostarczanego oprogramowania oparta o testy

187 views

Published on

Na bazie swoich doświadczeń Paweł opowie o tym jak pisanie testów pozwala zaoszczędzić czas i pomaga stać się lepszym programistą. Postara się odpowiedzieć na pytanie dlaczego tak często nie piszemy testów i pokazać na co należy uważać przy ich pisaniu.

Paweł to programista PHP i WebDeveloper z 7 letnim stażem. Przez ostatnie 2 lata leader zespołu a od niedawna Head of IT w Gdańskiej firmie z branży FinTech.

Published in: Technology
  • D0WNL0AD FULL ▶ ▶ ▶ ▶ http://1lite.top/4W1j9z ◀ ◀ ◀ ◀
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Be the first to like this

Jakość dostarczanego oprogramowania oparta o testy

  1. 1. OPARTA O DOBRE TESTY JAKOŚĆ DOSTARCZANEGO OPROGRAMOWANIA
  2. 2. SOFTWARE ENGINEER & WEB DEVELOPER PAWEŁ TEKLIŃSKI
  3. 3. DOBRY KOD • spełnia wymagania biznesowe • jest zrozumiały przez zespół • jest łatwy w utrzymaniu • jest testowalny • jest przetestowany
  4. 4. PROBLEM
  5. 5. PROBLEM • bardzo dużo kodu to spaghetti code • bardzo dużo kodu jest nie otestowane • testy wydają się stratą czasu • programiści ulegają presji • „u mnie działa” lub „przeklikałem” • awersja do testów jednostkowych
  6. 6. PROBLEM • bardzo dużo kodu to spaghetti code • bardzo dużo kodu jest nie otestowane • testy wydają się stratą czasu • programiści ulegają presji • „u mnie działa” lub „przeklikałem” • awersja do testów jednostkowych
  7. 7. KTO SIĘ NIE ZGODZI?
  8. 8. KIEDY, JAK I CO UŻYĆ BY TESTOWAĆ SWÓJ KOD
  9. 9. KIEDY
  10. 10. KIEDY A nie za wszelką cenę (not always) B C D
  11. 11. KIEDY A nie za wszelką cenę (not always) B naprawiając błędy (bugs) C D
  12. 12. KIEDY A nie za wszelką cenę (not always) B naprawiając błędy (bugs) C do skomplikowanej logiki (complex logic) D
  13. 13. KIEDY A nie za wszelką cenę (not always) B naprawiając błędy (bugs) C do skomplikowanej logiki (complex logic) D implementując logikę biznesową (domain)
  14. 14. JAK
  15. 15. *https://techblog.workiva.com/tech-blog/engineering-quality JAK
  16. 16. *https://techblog.workiva.com/tech-blog/engineering-quality JAK (NIE ROBIĆ)
  17. 17. *https://techblog.workiva.com/tech-blog/engineering-quality JAK
  18. 18. CZYM
  19. 19. PHPUnit PHPSpec *https://techblog.workiva.com/tech-blog/engineering-quality
  20. 20. PHPUNIT „PHPUnit is a unit testing framework for the PHP programming language. (…)” — Definition rom wikipedia
  21. 21. PHPSPEC „phpspec is a tool which can help you write clean and working PHP code using behaviour driven development or BDD” — Introduction from phpspec manual
  22. 22. PRZYKŁADY
  23. 23. •mamy zaimplementować narzędzie do wysyłania powiadomień SMS •w celu wysłania SMS należy zrobić request POST z odpowiednim payloadem •do URLa trzeba dodać secret do autoryzacji •dać info czy się udało czy nie
  24. 24. class Notifier { private $url = 'https://bramka.sms'; private $secret = 'ABCD'; public function sendWelcomeSms(User $user) { $url = $this->url . "?secret=" . $this->secret; $data = [ 'number' => $user->getPhoneNumber(), 'message' => 'Hello ' . $user->getName(), ]; $options = [ 'http' => [ 'header' => "Content-type: application/x-www-form urlencodedrn”, 'method' => 'POST', 'content' => http_build_query($data) ] ]; $context = stream_context_create($options); return file_get_contents($url, false, $context); } }
  25. 25. class Notifier { private $url = 'https://bramka.sms'; private $secret = 'ABCD'; private $httpClient; public function __construct(Client $httpClient) { $this->httpClient = $httpClient; } public function sendWelcomeSms(User $user) { $url = $this->url . "?secret=" . $this->secret; $data = [ 'number' => $user->getPhoneNumber(), 'message' => 'Hello ' . $user->getName(), ]; $response = $this->httpClient->post($url, $data); return $response->getStatusCode() === 200; } }
  26. 26. class NotifierSpec extends ObjectBehavior { function let(Client $client) { $this->beConstructedWith($client); } function it_sends_welcome_sms( Client $client, ResponseInterface $response ) { $response->getStatusCode()->willReturn(200); $client ->post('https://bramka.sms?secret=ABCD', [ 'number' => '500600700', 'message' => 'Hello Joe Doe' ]) ->willReturn($response); $user = new User('Joe Doe', ‚500600700'); $this->sendWelcomeSms($user)->shouldBe(true); } }
  27. 27. class NotifierSpec extends ObjectBehavior { function let(SmsProvider $smsProvider) { $this->beConstructedWith($smsProvider); } function it_sends_welcome_sms(SmsProvider $smsProvider) { $smsProvider ->send('500600700', 'Hello Joe Doe’) ->shouldBeCalled() ->willReturn(true); $user = new User('Joe Doe', ‚500600700'); $this->sendWelcomeSms($user)->shouldBe(true); } } interface SmsProvider { public function send($phoneNumber, $message); }
  28. 28. class Notifier { private $smsProvider; public function __construct(SmsProvider $smsProvider) { $this->smsProvider = $smsProvider; } public function sendWelcomeMessage(User $user) { $message = 'Hello ' . $user->getName(); return $this->smsProvider ->send($user->getPhoneNumber(), $message); } }
  29. 29. •trafiliśmy do projektu w którym korzysta się z Symfony •musimy napisać kontroler i 2 akcje (pokaż i usuń produkt) •po usunięciu produktu musimy pokazać wiadomość, że produkt został usunięty
  30. 30. class PatientControllerSpec extends ObjectBehavior { /** * @param SymfonyComponentDependencyInjectionContainerInterface $container * @param SymfonyComponentSecurityCoreSecurityContextInterface $securityContext * (…) * @param WhiteOctoberBreadcrumbsBundleModelBreadcrumbs $breadcrumbs */ function let( $container, $securityContext, $doctrine, $pRepository, $cRepository, $manager, $user, $token, $request, $attributes, $session, $flashBag, $router, $mainMenu, $itemInterface, $breadcrumbs, MenuItem $patientMenu, MenuItem $navbarMenu, RoleChecker $roleChecker, EngineInterface $templating, Paginator $paginator, PlanChecker $planChecker, FormFactory $formFactory ) { $mainMenu->offsetGet("scheduler")->willReturn($itemInterface); $patientMenu->offsetGet("EditData")->willReturn($itemInterface); $navbarMenu->offsetGet("schedulerNab")->willReturn($itemInterface); $itemInterface->setCurrent(true)->willReturn(true); $request->attributes = $attributes; $token->getUser()->willReturn($user); $securityContext->getToken()->willReturn($token); $container->has('security.context')->willReturn(true); $container->get('security.context')->willReturn($securityContext); $container->has('doctrine')->willReturn(true); $container->get('doctrine')->willReturn($doctrine); $container->get('request')->willReturn($request); $container->has('session')->willReturn(true); $container->get('session')->willReturn($session); $container->get('router')->willReturn($router); $container->get('app.scheduler.menu.main')->willReturn($mainMenu); $container->get('schedulerNab')->willReturn($pMenu); $container->get('app.scheduler.navbar_menu.left')->willReturn($navbarMenu); $container->get('white_october_breadcrumbs')->willReturn($breadcrumbs); $container->get('app.user.role_checker')->willReturn($roleChecker); $container->get('knp_paginator')->willReturn($paginator); $container->get('templating')->willReturn($templating); $container->get('app.clinic.plan_checker')->willReturn($planChecker); $container->get('form.factory')->willReturn($formFactory); $session->getFlashBag()->willReturn($flashBag); $manager->getRepository(‚App:P')->willReturn($pRepository); $manager->getRepository('App:C')->willReturn($cRepository); $doctrine->getManager()->willReturn($manager); $this->setContainer($container); } function it_is_controller() { $this->shouldHaveType('AppControllerController'); } } 23
  31. 31. public function showAction($productId) { $product = $this->getDoctrine() ->getRepository(Product::class) ->find($productId); return $this->render('App::products.html.twig', [ 'product' => $product ]); }
  32. 32. function it_shows_product( ContainerInterface $container, RegistryInterface $doctrine, ObjectRepository $repository, EngineInterface $templating) { $product = new Product(); $response = new Response('My response'); $container->has('doctrine')->willReturn(true); $container->get('doctrine')->willReturn($doctrine); $doctrine ->getRepository(Product::class) ->willReturn($repository); $repository->find(Argument::any())->willReturn($product); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), ['product' => $product], null) ->willReturn($response); $this->showAction(123) ->getContent() ->shouldContain('My response'); }
  33. 33. function it_shows_product( ContainerInterface $container, RegistryInterface $doctrine, ObjectRepository $repository, EngineInterface $templating) { $product = new Product(); $response = new Response('My response'); $container->has('doctrine')->willReturn(true); $container->get('doctrine')->willReturn($doctrine); $doctrine ->getRepository(Product::class) ->willReturn($repository); $repository->find(Argument::any())->willReturn($product); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), ['product' => $product], null) ->willReturn($response); $this->showAction(123) ->getContent() ->shouldContain('My response'); }
  34. 34. public function showAction($productId) { $product = $this->getDoctrine() ->getRepository(Product::class) ->find($productId); return $this->render('App::products.html.twig', [ 'product' => $product ]); } public function deleteAction($productId) { $product = $this->getDoctrine() ->getRepository(Product::class) ->find($productId); $em = $this->getDoctrine()->getEntityManager(); $em->remove($product); $em->flush(); $this->addFlash( 'notice', 'Product removed' ); return $this->render('App::products.html.twig', []); }
  35. 35. function it_deletes_product( ContainerInterface $container, RegistryInterface $doctrine, ObjectRepository $repository, EngineInterface $templating, ObjectManager $entityManager, Session $session, FlashBagInterface $flashBag) { $product = new Product(); $response = new Response('My response'); $container->has('doctrine')->willReturn(true); $container->get('doctrine')->willReturn($doctrine); $doctrine ->getRepository(Product::class) ->willReturn($repository); $doctrine ->getEntityManager() ->willReturn($entityManager); $repository->find(Argument::any())->willReturn($product); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), [], null) ->willReturn($response); $container->has('session')->willReturn(true); $container->get('session')->willReturn($session); $session->getFlashBag()->willReturn($flashBag); $this->deleteAction(123); $entityManager->remove($product)->shouldHaveBeenCalled(); $entityManager->flush()->shouldHaveBeenCalled(); $flashBag->add('notice', 'Product removed')->shouldHaveBeenCalled(); } 9
  36. 36. function it_deletes_product( ContainerInterface $container, RegistryInterface $doctrine, ObjectRepository $repository, EngineInterface $templating, ObjectManager $entityManager, Session $session, FlashBagInterface $flashBag) { $product = new Product(); $response = new Response('My response'); $container->has('doctrine')->willReturn(true); $container->get('doctrine')->willReturn($doctrine); $doctrine ->getRepository(Product::class) ->willReturn($repository); $doctrine ->getEntityManager() ->willReturn($entityManager); $repository->find(Argument::any())->willReturn($product); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), [], null) ->willReturn($response); $container->has('session')->willReturn(true); $container->get('session')->willReturn($session); $session->getFlashBag()->willReturn($flashBag); $this->deleteAction(123); $entityManager->remove($product)->shouldHaveBeenCalled(); $entityManager->flush()->shouldHaveBeenCalled(); $flashBag->add('notice', 'Product removed')->shouldHaveBeenCalled(); }
  37. 37. function it_deletes_product( ContainerInterface $container, ProductRepository $productRepository, FlashMessages $flashMessages, EngineInterface $templating) { $product = new Product(); $response = new Response('My response'); $container->get('product_repository')->willReturn($productRepository); $productRepository->find(Argument::any())->willReturn($product); $container->get('flash_messages')->willReturn($flashMessages); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), [], null) ->willReturn($response); $this->deleteAction(123); $repository->remove($product)->shouldHaveBeenCalled(); $flashMessages->addNotice('Product removed')->shouldHaveBeenCalled(); }
  38. 38. public function showAction($productId) { $product = $this->get('product_repository')->find($productId); return $this->render('App::products.html.twig', [ 'product' => $product ]); } public function deleteAction($productId) { $product = $this->get('product_repository')->find($productId); $this->get('product_repository')->remove($product); $this->get('flash_messages')->addNotice('Product removed'); return $this->render('App::products.html.twig', []); } • po usunięciu produktu trzeba wysłać DELETE do API
  39. 39. function it_deletes_product( ContainerInterface $container, ProductRepository $productRepository, FlashMessages $flashMessages, ApiInfomer $apiInformer, EngineInterface $templating) { $product = new Product(); $response = new Response('My response'); $container->get(‚product_repository')->willReturn($productRepository); $productRepository->find(Argument::any())->willReturn($product); $container->get('flash_messages')->willReturn($flashMessages); $container->get('api_informer')->willReturn($apiInformer); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), [], null) ->willReturn($response); $this->deleteAction(123); $repository->remove(123)->shouldHaveBeenCalled(); $apiInformer->send('DELETE', 123)->shouldHaveBeenCalled(); $flashMessages->addNotice('Product removed')->shouldHaveBeenCalled(); }
  40. 40. function it_deletes_product( ContainerInterface $container, ProductRepository $productRepository, FlashMessages $flashMessages, NotifyProvider $notifyProvider, EngineInterface $templating) { $product = new Product(); $response = new Response('My response'); $container->get(‚product_repository')->willReturn($productRepository); $productRepository->find(Argument::any())->willReturn($product); $container->get('flash_messages')->willReturn($flashMessages); $container->get('notify_provider')->willReturn($notifyProvider); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), [], null) ->willReturn($response); $this->deleteAction(123); $repository->remove(123)->shouldHaveBeenCalled(); $notifyProvider->removedProduct($product)->shouldHaveBeenCalled(); $notifyProvider->notify()->shouldHaveBeenCalled(); $flashMessages->addNotice('Product removed')->shouldHaveBeenCalled(); }
  41. 41. function it_deletes_product( ContainerInterface $container, DeleteProductUseCase $useCase, EngineInterface $templating) { $response = new Response('My response'); $container->get('delete_product_use_case')->willReturn($useCase); $container->has('templating')->willReturn(true); $container->get('templating')->willReturn($templating); $templating ->render(Argument::any(), [], null) ->willReturn($response); $this->deleteAction(123); $useCase->run(Argument::that(function ($command) { return $command->getProductId() === 123; }))->shouldHaveBeenCalled(); }
  42. 42. public function showAction($productId) { $product = $this->get('product_repository')->find($productId); return $this->render('App::products.html.twig', [ 'product' => $product ]); } public function deleteAction($productId) { $command = new DeleteProduct($productId); $this->get('delete_product_use_case')->run($command); return $this->render('App::products.html.twig', []); }
  43. 43. • dzięki testom zdefiniowaliśmy interfejsy • dzięki testom udało nam się wynieść kod na zewnątrz • dzięki skupieniu się na zachowaniach mamy opisaną komunikację a to podstawa OOP
  44. 44. TIPS
  45. 45. public function testNotifyRecipient() { $recipient = new Recipient('AGY', 'Joe','Doe', '500600700'); $smsProvider = new SmsProvider($someDependency); $logger = new NotificationLogger($someDependency); $notifyUseCase = new NotifyUseCase( $smsProvider, $logger ); $notifyUseCase->notify($recipient); $this->assertEquals(true, $logger->welcomeWasSend()); } TO NIE UNIT TEST
  46. 46. function its_serialize_to_format(Recipient $recipient) { $recipient->getFullName()->willReturn('Jan Kowalski'); $recipient->getIdentity()->willReturn('AGY'); $serializedStr = '99999999;"25/2002";"AGY";"Jan Kowalski";0;;0'; $this->serialize($recipient)->shouldBe($serializedStr); } VALUE OBJECT’S
  47. 47. function its_serialize_to_format(Recipient $recipient) { $recipient->getFullName()->willReturn('Jan Kowalski'); $recipient->getIdentity()->willReturn('AGY'); $serializedStr = '99999999;"25/2002";"AGY";"Jan Kowalski";0;;0'; $this->serialize($recipient)->shouldBe($serializedStr); } —————————————————————————————————————————————————————————————- - its serialize to format could not reflect class ModelRecipient as it is marked final. VALUE OBJECT’S
  48. 48. class UserRepository extends EntityRepository { public function findByIdentity($identity) { return $this->findOneBy(['identity' => $identity]); } } (NIE) MOCKUJ CO NIE TWOJE
  49. 49. function let(EntityManager $manager, ClassMetadata $metadata) { $this->beConstructedWith($manager, $metadata); } function it_finds_user_by_identity( EntityManager $manager, UnitOfWork $unitofwork, EntityPersister $entityPersister ) { $user = new User(); $manager->getUnitOfWork()->willReturn($unitofwork); $unitofwork->getEntityPersister(Argument::any())- >willReturn($entityPersister); $entityPersister->load(['identity' => '123'], null, null, [], null, 1, null) ->willReturn($user); $identity = '123'; $this->findByIdentity($identity)->shouldBe($user); }
  50. 50. function let(EntityManager $manager, ClassMetadata $metadata) { $this->beConstructedWith($manager, $metadata); } function it_finds_user_by_identity( EntityManager $manager, UnitOfWork $unitofwork, EntityPersister $entityPersister ) { $user = new User(); $manager->getUnitOfWork()->willReturn($unitofwork); $unitofwork->getEntityPersister(Argument::any())- >willReturn($entityPersister); $entityPersister->load(['identity' => '123'], null, null, [], null, 1, null) ->willReturn($user); $identity = '123'; $this->findByIdentity($identity)->shouldBe($user); }
  51. 51. interface UserRepository { public function findByIdentity($identity); } class DoctrineUserRepository extends EntityRepository implements UserRepository { public function findByIdentity($identity) { return $this->findOneBy(['identity' => $identity]); } }
  52. 52. class AddressTypeSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('SymfonyComponentFormAbstractType'); $this->shouldHaveType('AppFormAddressType'); } function it_builds_form(FormBuilderInterface $builder) { $builder->add(Argument::cetera())->willReturn($builder); $builder->add('street', 'text', Argument::any())->shouldBeCalled()->willReturn($builder); $builder->add('city', 'text', Argument::any())->shouldBeCalled()->willReturn($builder); $builder->add('number', 'text', Argument::any())->shouldBeCalled()->willReturn($builder); $builder->add('areaCode', 'text', Argument::any())->shouldBeCalled()->willReturn($builder); $builder->add('flatNumber', 'text', Argument::any())->shouldBeCalled() ->willReturn($builder); $this->buildForm($builder, []); } function it_has_valid_name() { $this->getName()->shouldReturn('app_address_type'); } function it_has_valid_data_class(OptionsResolverInterface $optionsResolver) { $optionsResolver->setDefaults(Argument::withEntry('data_class', ‚AppEntityAddress')) ->shouldBeCalled(); $this->setDefaultOptions($optionsResolver); } } (NIE) TESTUJ FORMÓW
  53. 53. class EntitySpec extends ObjectBehavior { public function it_gets_all_services( Service $service1, Service $service2 ) { $this->addService($service1); $this->addService($service2); $this->getServices()->shouldBe([$service1, $service2]); } } (NIE) TESTUJ SET/GET
  54. 54. ZŁE METRYKI • Code coverage (ilościowy) • Test execution (ilościowy) • Percentage of tests passed or failed (ilościowy)
  55. 55. DOBRE METRYKI • Cyclomatic Complexity (jakościowy) • Total test duration (ilościowy) • Defects in production (ilościowo/jakościowy)
  56. 56. PODSUMUJMY • zasada ABCD • odpowiednie unit testy potrafią pokazać wady projektowe • nie bazuj na jednej metryce • pamiętaj, że poza unit testami jest coś jeszcze
  57. 57. https://www.youtube.com/watch?v=0GypdsJulKE
  58. 58. TESTS WON’T MAKE YOU A BETTER DEV BUT THEY’LL MAKE YOU MORE CONFIDENT — SILVANO STRALLA
  59. 59. THEN YOU’LL FIND THE TIME TO BECOME A BETTER DEV — SILVANO STRALLA
  60. 60. THANKS!

×