Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TDD: The Good Parts

TDD: The Good Parts

A look at what makes good code, how TDD can help, how it can get in the way, and strategies for testing first without focusing only on testability.

Adam Wathan

May 29, 2014
Tweet

More Decks by Adam Wathan

Other Decks in Programming

Transcript

  1. What makes good code? Easy to change 4 No duplication

    4 Low coupling 4 Separation of concerns
  2. What makes good code? Enjoyable to use 4 Intuitive public

    API 4 Forgiving and flexible 4 It just works
  3. // OrderItemTest.php public function test_can_get_total_price() { $product = M::mock('Product'); $product->shouldReceive('getPrice')->andReturn(500);

    $orderItem = new OrderItem($product, 3); $this->assertEquals(1500, $orderItem->getTotalPrice()); } Seems reasonable...
  4. Requirements change and now $product->getPrice() needs to be renamed to

    $product->getSellingPrice()... // ProductTest.php public function test_can_get_selling_price() { $product = new Product('Shampoo', 600); $this->assertEquals(600, $product->getSellingPrice()); }
  5. Oh noes! 4 Tests still pass but app is broken!

    4 Stub product in OrderItemTest is now out of sync with the real Product 4 Our tests are lying!
  6. // OrderItemTest.php public function test_can_get_total_price() { $product = new Product('foobar',

    500); $orderItem = new OrderItem($product, 3); $this->assertEquals(1500, $orderItem->getTotalPrice()); } Now this test will actually fail when we change the API of Product!
  7. When your tests use the same collaborators as your application,

    they always break when they should. 1 Sandi Metz
  8. The great irony of over- isolation is that it actually

    makes you more dependent on implementation. 1 Steve Fenton
  9. // Order.php public function getTotalPrice() { $result = 0; foreach

    ($this->items as $item) { $result += $item->getTotalPrice(); } return $result; } ...but this implementation fails!
  10. // Order.php public function getTotalPrice() { return $this->items->reduce(function($carry, $item) {

    return $carry += $item->getTotalPrice(); }, 0); } ...and this one fails...
  11. // Order.php public function getTotalPrice() { $result = 0; for

    ($i = 0; $i < count($this->items); $i++) { $result += $this->items[$i]->getTotalPrice(); } return $result; } ...also fails!
  12. // OrderTest.php public function test_can_get_total_price() { $shampoo = new Product('Shampoo',

    300); $toothpaste = new Product('Toothpaste', 500); $deodorant = new Product('Deodorant', 700); $items = new Collection([ new OrderItem($shampoo, 1), new OrderItem($toothpaste, 1), new OrderItem($deodorant, 1), ]); $order = new Order($items); $this->assertEquals(1500, $order->getTotalPrice()); } All implementations are green!
  13. We need to get a customer's unshipped orders. This would

    be a nice way to get them... $customer->getOpenOrders();
  14. Trying to test in isolation... // CustomerTest.php public function test_it_can_retrieve_open_orders()

    { $open_orders = M::mock('OrderCollection'); $customer = M::mock('Customer[orders]'); $customer->shouldReceive('orders->whereUnshipped->get') ->andReturn($open_orders); $this->assertEquals($open_orders, $customer->getOpenOrders()); }
  15. // OrderRepositoryTest.php public function test_it_can_retrieve_open_orders() { $database = M::mock('DatabaseConnection'); $order_mapper

    = M::mock('OrderMapper'); $customer = M::mock('Customer') $order = M::mock('Order'); $open_orders = [$order]; $mock_db_results = M::mock(); $database->shouldReceive('table->whereNull->get') ->andReturn($mock_db_results); $order_mapper->shouldReceive('mapFromDatabase') ->with($mock_db_results) ->andReturn($open_orders); $orderRepository = new OrderRepository($database, $order_mapper); $expected = $open_orders; $actual = $orderRepository->getOpenOrdersForCustomer($customer); $this->assertEquals($expected, $actual); }
  16. // OrderRepository.php public function getOpenOrdersForCustomer($customer) { $customer_id = $customer->getId(); $data

    = $this->db->table('orders')->whereNull('date_shipped')->get(); $orders = $this->order_mapper->mapFromDatabase($data); return $orders; }
  17. // OrderMapperTest.php public function test_it_can_map_database_results_to_orders() { $database_results = [ [

    'id' => '1', 'customer_id' => '1', 'date_shipped' => '2013-04-01', 'date_ordered' => '2013-03-28', 'payment_method' => '1' ], [ 'id' => '2', 'customer_id' => '1', 'date_shipped' => '2012-03-06', 'date_ordered' => '2012-03-01', 'payment_method' => '1' ], [ 'id' => '3', 'customer_id' => '1', 'date_shipped' => '2014-01-15', 'date_ordered' => '2014-01-12', 'payment_method' => '3' ], ]; // Setup expected orders, blah blah blah... $mapper = new OrderMapper; $actual = $mapper->mapFromDatabase($database_results); $this->assertEquals($expected, $actual); }
  18. // OrderMapper.php public function mapFromDatabase($database_results) { $orders = []; foreach

    ($database_results as $row) { $orders[] = $this->mapDatabaseRow($row); } return $orders; }
  19. // OrderMapper.php public function mapDatabaseRow($row) { $order = new Order;

    $order->setId($row['id']); $order->setCustomerId($row['customer_id']); $order->setDateShipped(new DateTime($row['date_shipped'])); $order->setDateOrdered(new DateTime($row['date_ordered'])); $order->setPaymentMethod($this->convertPaymentMethod($row['payment_method'])); return $order; }
  20. $database = new DatabaseConnection($credentials); $customerMapper = new CustomerMapper; $customerRepository =

    new CustomerRepository($database, $customerMapper); $customer = $customerRepository->findById(1); $orderMapper = new OrderMapper; $orderRepository = new OrderRepository($database, $orderMapper); $orders = $orderRepository->getOpenOrdersForCustomer($customer);
  21. // CustomerTest.php public function test_it_can_retrieve_open_orders() { $customer = new Customer;

    $shipped_order1 = new Order(['date_shipped' => new DateTime]); $shipped_order2 = new Order(['date_shipped' => new DateTime]); $unshipped_order = new Order(['date_shipped' => null]); $customer->orders()->save([$shipped_order1, $shipped_order2, $unshipped_order]); $open_orders = $customer->getOpenOrders(); $this->assertTrue($open_orders->contains($unshipped_order)); $this->assertFalse($open_orders->contains($shipped_order1)); $this->assertFalse($open_orders->contains($shipped_order2)); }
  22. If talking to the database is stable and fast then

    there's no reason not to do it in your unit tests. 1 Martin Fowler
  23. The Database isn't Evil 4 Use an in-memory SQLite database

    that you can re- migrate for each test 4 Super fast 4 Makes it easy to test ActiveRecord-style objects
  24. Testing is an art 4 No magic steps or rules

    4 Everything has trade-offs 4 Everything takes practice and experience 4 Isolate because your system needs it, not for the sake of tests