Test DRIVEN DEVELOPMENT

für

Datenwissenschaftler

Was ist das Schwierigste Problem in der IT?

Stabilität!

Warum?

Weil alles leicht zu ändern ist

Lösung 1

Ein wirklich Strikter Compiler

ADA

  • Programmiersprache benannt nach Ada Lovelace
  • Sie war der erste Programmierer überhaupt
  • ADA wird verwendet (hoffentlich!?) für Militär, Raketen, Atomkraftwerke etc.
type Day_type   is range    1 ..   31;
type Month_type is range    1 ..   12;
type Year_type  is range 1800 .. 2100;
type Hours is mod 24;
type Weekday is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);

type Date is
   record
     Day   : Day_type;
     Month : Month_type;
     Year  : Year_type;
   end record;

ADA Beispiel

ADA NAchteile

  • extrem komplexe Sprache

  • extrem strenge Regeln

  • extrem aufwändiges, langsames und teures Programmieren

Lösung 2

TEST Driven Development

  • Beck, K. (2003): Test-Driven Development by Example
  • Automatisierte Tests
  • Schwerpunkt auf Unittests
  • Modernere Sichtweise
    • Martin (2008): Clean Code

Vokabeln

  • Unit Tests
    • schnell, in-memory
    • keine externen Abhängigkeiten
  • Integration Tests
    • in-memory
    • Abhängigkeiten wie DBs in-memory
  • End-to-End Tests
    • möglichst nahe am Produktionssystem
    • Docker-Container
    • Abhängigkeiten werden mitgetestet

Perfekte Welt

  • Test programmieren bevor man die Funktionalität programmiert
  • Test schlägt zunächst fehl
  • Solange programmieren, bis der Test "grün" ist
  • Sämtlicher Code ist auf diese Weise programmiert und damit auch getestet

REALE Welt

  • Legacy Code ohne Tests
  • Test als "Dokumentation" im Nachhinein hinzufügen
  • Tests um Bugs zu reproduzieren
  • Code-Abdeckung 0-50%

DATA Science Welt

  • Python Spaghetti-Code mit vielen globalen Variablen
  • verteilt auf mehrere Jupyter Notebooks
  • Nicht reproduzierbare Umgebung
  • keine Tests
  • keine Dokumentation
  • kein wirkliches Versionsmanagment 
    • Jupyter Notebook Format erlaubt keine richtigen Diffs
    • Versionsmanagment von Daten scheitert oft an der Datenmenge, die zu groß ist um sie mit git zu versionieren

Schritte zur Verbesserung

  1. Benutze git (auch für Daten bis 100MB)
  2. Benutze virtualenv, requirements.txt, pip, pipenv oder poetry um die Umgebung reproduzierbar wiederherstellen zu können
  3. Bewege Code aus Jupyter Notebooks in richtige Python-Dateien
  4. Keine def und class Definitionen in Jupyter Notebooks!
  5. Nutze Jupyter Notebooks als "manuelle" Tests!
  6. Nutze Python Skripts statt Jupyter Notebooks für wiederkehrende Aufgaben (z.B. regelmäßige Datenbankupdates, Webseiten-Scrapings etc.)

Pytest

  • Installation
    • pipenv install --dev pytest pytest-mock 
  • Nutzung
    • pytest -s 
      • = test session starts=
        platform darwin -- Python 3.6.5, pytest-4.3.1, py-1.8.0, pluggy-0.9.0
        rootdir: <PATH>, inifile:
        plugins: mock-1.10.1
        collected 0 items 
        = no tests ran in 0.01 seconds =========
    • Standardmäßig sucht pytest nach 
      • Dateien mit test_ *.py oder *_test.py

      • Funktionen mit Namen test*
      • Klassen mit Namen Test*

Erstes Beispiel

# content of test_sample.py
def func(x):
    return x + 1

def test_answer():
    assert func(3) == 5 
$ pytest
= test session starts=
...
collected 1 item

test_sample.py F                                                     [100%]
= FAILURES =
_ test_answer _
    def test_answer():
>       assert func(3) == 5
E       assert 4 == 5
E        +  where 4 = func(3)

test_sample.py:5: AssertionError
= 1 failed in 0.12 seconds =

Eine TESTKlasse

from elasticsearch import Elasticsearch, RequestError
import elasticwrapper as ew
import pytest
# Test needs real elasticsearch (Integration/End2end-Test)
class TestApp(object):
    @classmethod
    def setup_class(cls):
        es_host = "http://35.242.247.72:9200/"
        cls.es = Elasticsearch(es_host, timeout=300)
    @classmethod
    def teardown_class(cls):
        print("Tests stopped!")
    def test_match_field_names_None(self): 
        with pytest.raises(RequestError): 
            result = ew.match_field_names(None, es=self.es, index="meta")

Mocking

import actions
# Test works without Elasticsearch (Unit test)
class TestApp(object):
    def test_get_index(self, mocker):
        mock_es = mocker.MagicMock()
        mock_es.configure_mock(**{"indices.exists_alias.return_value": False})
        index = actions.get_index("product_finder", mock_es)
        assert index is None
        mock_es.indices.exists_alias.assert_called_once()

        # check that we don't call elasticsearch if we have the index name in cache
        mock_es.reset_mock()
        index = actions.get_index("product_finder", mock_es, cache={"product_finder": "cached_name"})
        assert index == "cached_name"
        mock_es.indices.exists_alias.assert_not_called()

Referenzen

ENDE

Test Driven Development

By andimai

Test Driven Development

für Datenwissenschaftler

  • 160