TDD is not JUST

about tests

Fabrizio Romano

@gianchub

ProgSCon - London

April 22th, 2016

Hello London!

Thank you for being here!

I'm a Computer Science Engineer, addicted to Python, clean code and books.

 

I'm the author of Learning Python (Packt Publishing, 12.2015).

About me

Sr. Platform Developer

@ student.com

We're

hiring!

The plan:

...and let's have some fun while we do it

  1. What drew me to TDD
  2. Why would you need it?
  3. A story about TDD

A little disclaimer

  • This is my own view
  • Simple examples
  • No definitions
  • Code is in Python, but
    Python is easy to read...

What drew me to TDD?

It all happened when I moved to London...

Don't worry, TDD will take us there

- Mark Henwood

Too much logic!

- Ondrej Kohout

A couple of colleagues were a great inspiration:

Why do we need TDD?

def is_positive(n):
    # We assume n is integer.
    return n > 0

Example #1

How do we test this function?

Boundaries

def is_positive(n):
    # We assume n is integer.
    return n > 0


eq(False, is_positive(0))
eq(False, is_positive(-3))
eq(True, is_positive(3))

Is this a good test?

def is_positive(n):
    # We assume n is integer.
    return n > 1


eq(False, is_positive(0))   # still passing
eq(False, is_positive(-3))  # still passing
eq(True, is_positive(3))    # still passing

Granularity

def is_positive(n):
    # We assume n is integer.
    return n > 1


eq(False, is_positive(0))   # still passing
eq(False, is_positive(-1))  # still passing
eq(True, is_positive(1))    # NOW FAILS!

Much better!

The boundary cannot jiggle any more!

def is_positive(n):
    # We assume n is integer.
    return n > 0


eq(False, is_positive(0))

for n in range(1, 10 ** 4):
    eq(False, is_positive(-n))
    eq(True, is_positive(n))

Even better!

But unit tests need to be FAST...

So, can we test everything?

def is_positive(n):
    # We assume n is integer.
    if n == 10 ** 16:
        return 'Hola!'
    return n > 0

If you could test 1'000'000'000 numbers a second, it would take about 4 months to spot this.

We cannot test everything.

def get_squares(v):
    # assumes v is a list of integers
    if not v:
        return []
    return [n ** 2 for n in v]

Example #2

How do we test this function?

def get_squares(v):
    # assumes v is a list of integers
    if not v:
        return []
    return [n ** 2 for n in v]


eq([1, 0, 4, 9], get_squares([-1, 0, 2, -3]))
eq([], get_squares([]))

But what about that redundancy?
We may not notice it, if we tested AFTERWARDS

def get_squares(v):
    # assumes v is a list of integers
    if not v:
        return []
    return [n ** 2 for n in v]
# this test would cause us to write get_squares2
eq([1, 0, 4, 9], get_squares2([-1, 0, 2, -3]))

# and this would automatically pass, thanks to
# the list comprehension
eq([], get_squares2([]))

Had we written the tests first, there would be no redundancy.

def get_squares2(v):
    # assumes v is a list of integers
    return [n ** 2 for n in v]

Better than solving these problems, is to avoid introducing them in the first place.

And that's where TDD comes into the game

So let's hear a story about TDD

Psst: It's extremely technical, beware!

Once Upon a Time...

there was a frog

He was in love with a beautiful princess. One day, at the pond, the frog took courage, jumped out of the water and told her:

I am a prince under a spell, kiss me,

break the spell and marry me!

Prince? Intriguing!

But she was a Geek...

TDD?

Let me get back

to you on this...

And he ran off to learn TDD from the masters...

Kent Beckowl

Robert Martinowl

TDD strooong

in them was...

TDD

Test Driven Development

TDD is a Software Development Process based on the repetition of a very short development cycle.

Def:

Steps

  • Short at first
  • With experience: longer
  • Trouble? Go back to short

Where does it start?

The frog thought the training was completed

But the masters disagreed,

and they kept giving examples...

What changes?

Without TDD

What & How

With TDD

What

How

OMG! It's like

having 2 brains!

TDD common aspects

  • KISS
  • YAGNI
  • Architecture design (during refactoring)
  • Three strikes and refactor
    (Test-Driven Development with Python - H. Percival)
  • Triangulation

?

Triangulation

eq(4, square(-2))


def square(n):
    # we can cheat, it is
    # the only requirement
    return 4

First step

"Fake it 'till you make it"

eq(4, square(-2))
eq(9, square(3))


def square(n):
    return n ** 2

With Triangulation

You write

the actual logic.

Main Benefits

  • Refactor with Confidence
  • Readability
  • Loose Coupling
  • Easier to test and maintain
  • Test first => Better understanding of requirements
  • Small units => easier debugging and tests as docs
  • Higher speed:
    It takes less to write tests and code
    than to write code and debug

Main Shortcomings

  • Whole company needs to believe
  • Blind spots
  • Badly written tests are hard to maintain
  • Hard to do it when dealing with new tools
  • Hard to do it when the task isn't fully specified, and we have exploration to do

Real life examples

How do you test legacy code?

Better way...

Changing a horribly long view

def get(request, *args, **kwargs):    
    
    # imagine many lines of code here...    

    data = get_data(**search_params)

    # here we work on the data and
    # eventually we put it in the context
    # dict we use to render the template

    return render(template_name, context, extra_params)

We need to insert pagination, filtering, sorting

def get(request, *args, **kwargs):
    
    # same as before
    
    original_data = get_data(**search_params)

    filtered_data = filter_data(
        original_data, **filter_params)

    sorted_data = sort_data(
        filtered_data, **sort_params)

    data = paginate_data(
        sorted_data, **pagination_params)
    
    # same as before

    return render(template_name, context, extra_params)

We code pagination, filtering and sorting with TDD

Introducing a new functionality in existing code

We need to add a feature to a long piece of untested code.

def very_long_function(*args, **kwargs):
    
    # nasty piece of code that does
    # a lot of things.

    return result

We don't have the time to write a comprehensive test suite for it.

One possible solution:

def test_new_functionality():
    # preparation stage

    result = very_long_function(*args, **kwargs)

    eq(expected_result, result)


def very_long_function(*args, **kwargs):
    
    # same nasty piece of code
    # with NEW FUNCTIONALITY IN

    return result

After all these examples,

the frog was in ZEN-Mode

He went back to the princess and passed the exam.

So they married, and when the minister said:

"You can kiss the bride"...

Nothing changed!

He was just a talking frog after all!

What's the moral of the story?

The princess should have tested FIRST!

And so should you

Thank you! Come say hi!

gianchub

gianchub

gianchub [at] gmail [dot] com

Made with Slides.com