środa, 25 kwietnia 2012

Tłumne zgromadzenie inspiruje, czyli o lekko bajeranckim testowaniu krotek słów kilka

Chwała!
Jak uważni, stali Czytelnicy wiedzą, ledwo co byłem na Najlepszej Z Konferencji. Jednakże któż by przypuszczał, że tak szybko nadarzy się okazja do praktycznego i zarobkowego wykorzystania inspiracji z tego wydarzenia?
Tym razem nadarzyło się wykorzystać zasiane ziarno pomysłów przez Bartka Kuczyńskiego, znanego też pod mianem Koziołka. I właściwie, najbardziej w tym wszystkim zaskakujące było, że sam wcześniej na to nie wpadłem.
 - No dobra, ale może wreszcie ktoś wyjaśni o co się rozchodzi? - zapytacie Państwo. No więc, detalicznie i szczegółowo rozchodzi się o rozwiązanie następującego problemu. Otóż, dostałem do sprawdzenia mechanizm czegoś, co można nazwać generowaniem raportu dla instytucji zewnętrznej. Raport ten jest zestawieniem sumarycznym danych zebranych z kilku tabel, przy różnych również warunkach wyboru odpowiednio od typu rozpatrywanego "głównego obiektu".
Oczywistym rozwiązaniem jest po prostu wejście w aplikację, stworzenie zestawu "obiektów" z danymi testowymi, uruchomienie generacji, i sprawdzenie wyników. Cóż, wszystko fajnie, jednakże stworzenie takiego zestawu byłoby dość czasochłonne. Zarówno samo w sobie, jak i dodatkowy narzut na dowiedzenie się lub przypomnienie jak się robiło poszczególne etapy. Do tego, nie zawsze da się wszystko tak ładnie sfabrykować, że miało miejsce jakiś większy czas temu.
No więc pomysł drugi. Jako, że testowany system, nomen omen, bazuje na bazie danych wiodącego producenta, a na środowisku testowym znajduje się kopia danych produkcyjnych to w zasadzie, dlaczego z nich nie skorzystać. Fajnie. SQL przecież, to potężne narzędzie. To do roboty... Ale... No tak. Struktura danych rozbudowana, do tego agregacje, zależności między danymi, a zapytaniami do wyboru następnych danych. A my tu nie jesteśmy na co dzień programistami sql'a, i znów jakoś praca zaczyna się nie kleić.
Ha! Ale na czym my się znamy? Co wykorzystujemy codziennie? I co może wyciągać też do dalszej obróbki dane z bazy danych? Tak, tak. Groovy. Dlaczego więc nie wykorzystać jego potencjału i napisać szybko skrypt, który przy użyciu prostych, szybkich do napisania zapytań sql pozwoli resztę logiki zakodować w znanym i lubianym języku? Chwila moment i powstało coś takiego:
import groovy.sql.Sql

    Sql sql = Sql.newInstance('jdbc:oracle:thin:@db_connection_details', 'user', 'pass', 'oracle.jdbc.OracleDriver');

    final def firstCaseStatement = {objectNumber ->
"""select *
from table1, table2, ...
where object_number = $objectNumber
"""
    };

    final def secondCaseStatement = {objectNumber ->
"""select *
from table1, table2, ...
where object_number = $objectNumber
"""
    };

    final def raportStatement = {objectNumber ->
"""select *
from raport_table
where object_number = $objectNumber
"""
    };

        def objectNumber = '1234567890';
        def statement = firstCaseStatement(objectNumber).toString();
        def rows = sql.rows(statement)

        def firstValues = [:].withDefault {0.0g}
        def secondValues = [:].withDefault {0.0g}

        rows.each {
            switch (it.something_type) {
                case 'A':
                    firstValues[(it.key.toString())]  = it.value;
                    break;
                case 'B':
                    secondValues[(it.key.toString())]  = it.value;
            }
        }

        statement = secondCaseStatement(objectNumber)
        rows = sql.rows(statement);

        rows.each {
            switch (it.something_type) {
                case 'A':
                    firstValues[(it.key.toString())]  = it.value;
                    break;
                case 'B':
                    secondValues[(it.key.toString())]  = it.value;
            }
        }

        def expected = sql.firstRow(raportStatement(objectNumber).toString());

        println firstValues;
        println secondValues;
        println expected;
Hmmm... Fajnie, ale tylko dla jednego "obiektu"? I jeszcze chciałoby się jakoś wygodnie mieć podgląd na wyniki i przebieg działania. Hmmm... A czym to łatwo załatwić? Co znamy już dobrze? Tak, tak. TestNG! Kolejna chwila moment i otrzymuję coś takiego:
import groovy.sql.Sql

import org.testng.annotations.Test
import org.testng.annotations.DataProvider

import static org.testng.Assert.assertEquals

class TestClass {

    Sql sql = Sql.newInstance('jdbc:oracle:thin:@db_connection_details', 'user', 'pass', 'oracle.jdbc.OracleDriver');

    final def firstCaseStatement = {objectNumber ->
"""select *
from table1, table2, ...
where object_number = $objectNumber
"""
    };

    final def secondCaseStatement = {objectNumber ->
"""select *
from table1, table2, ...
where object_number = $objectNumber
"""
    };

    final def raportStatement = {objectNumber ->
"""select *
from raport_table
where object_number = $objectNumber
"""
    };

    @Test(dataProvider = 'objects')
    void test(def objectNumber) {
        def statement = firstCaseStatement(objectNumber).toString();
        def rows = sql.rows(statement)

        def firstValues = [:].withDefault {0.0g}
        def secondValues = [:].withDefault {0.0g}

        rows.each {
            switch (it.something_type) {
                case 'A':
                    firstValues[(it.key.toString())]  = it.value;
                    break;
                case 'B':
                    secondValues[(it.key.toString())]  = it.value;
            }
        }

        statement = secondCaseStatement(objectNumber)
        rows = sql.rows(statement);

        rows.each {
            switch (it.something_type) {
                case 'A':
                    firstValues[(it.key.toString())]  = it.value;
                    break;
                case 'B':
                    secondValues[(it.key.toString())]  = it.value;
            }
        }

        def expected = sql.firstRow(raportStatement(objectNumber).toString());

        def firstResult = verifyFirstValues(expected, firstValues);
        def secondResult = verifySecondValues(expected, secondValues);

        def result = new Pair<Boolean, String>(firstResult.first && secondResult.first, """${firstResult.second}
${secondResult.second}""");

        assertTrue(result.first, result.second);
    }


    Pair<Boolean, String> verifyFirstValues(def expected, def values) {
        def verifiedValues = [];
        def verificationMessage = new StringBuilder('First values ');
        def verificationResult = true;

        //Verification algorithm, which also add log to StringBuilder

        return new Pair<Boolean, String>(verificationResult, verificationMessage.toString());
    }

    Pair<Boolean, String> verifySecondValues(def expected, def values) {
       //analogic...
    }

    @DataProvider(parallel = true, name = 'objects')
    Iterator<Object[]> dataProvider() {
        def objectStatement = """select object_number
from object_table""";

        def rows = sql.rows(objectStatement, 0, 1000);

        def a = rows.collect {[it.object_number.toString()].toArray()};

        return a.iterator();
    }
}
Co dostałem w praktyce? Po pierwsze, dostawca danych (@DataProvider) załatwia mi uruchomienie sprawdzania dla dowolnie wybranej ilości "obiektów". Po drugie, jednym parametrem program zamienia się w wielowątkowy, co pozwala oszczędzić ciut czasu. Po trzecie, mam wygodny podgląd na to, co się dzieje po uruchomieniu testów (a mleć to potrafi powyżej kwadransa). Po czwarte wyniki mam od razu zebrane w raport pięknie pokazujący co, i jak poszło nie tak (lub tak). Jednym słowem bajka! A przecież nie jest to jeszcze napisane "optymalnie" i można by szybko parę bajerów dodać.

Brak komentarzy:

Prześlij komentarz