Proces tworzenia aplikacji biznesowych przypomina linię produkcyjną, na której każda partia przechodzi te same bramki jakości. Różnica polega na tym, że wytwarzany jest kod, a bramką jest test albo akceptacja użytkownika. Poniżej opisany jest cykl wytwarzania od wymagań po wydanie, ze szczególnym naciskiem na dwa obszary, które najczęściej decydują o stabilności po wdrożeniu: testy i wersjonowanie.

Tekst dotyczy aplikacji internetowych opartych na C# i SQL Server, czyli stosu, w którym SoftwareStudio wytwarza własne systemy. Ogólne zasady stosują się też do aplikacji mobilnych na Androida, choć dochodzi w nich dystrybucja pakietów.

Proces tworzenia aplikacji biznesowych - cykl wytwarzania

Cykl wytwarzania składa się z powtarzalnych faz, które w każdej iteracji dotyczą niewielkiego kawałka funkcjonalności. Krótka iteracja daje użytkownikom szybką informację zwrotną, a zespołowi wczesne wykrycie błędnych założeń. Dłuższe iteracje mają sens tylko tam, gdzie wdrożenie na środowisko testowe jest kosztowne.

Faza cykluCo się w niej dziejeArtefakt
Planowanie iteracjiWybór pozycji z listy wymagań, oszacowanie pracochłonnościZakres iteracji
ImplementacjaKodowanie na gałęzi funkcjonalnej, testy jednostkowePull request
Przegląd koduCzytanie zmian przez drugą osobę i poprawki po uwagachZatwierdzona zmiana
IntegracjaScalenie z gałęzią główną oraz automatyczny build z testamiNumer builda
DemonstracjaPokaz użytkownikom na środowisku testowymLista uwag
WydanieWdrożenie na produkcję według planu, notatki do wydaniaOznaczona wersja

Reguła: zmiana trafia na gałąź główną dopiero po przeglądzie i po zielonym wyniku automatycznych testów.

Zespół, który stosuje tę regułę, ma zawsze budowalną wersję gałęzi głównej. To upraszcza wydania, bo wydanie polega na oznaczeniu istniejącego stanu, a nie na sklejaniu zmian na ostatnią chwilę.

Szacowanie i planowanie iteracji

Pracochłonność szacuje się w godzinach, podając zakres zamiast jednej liczby, bo niepewność jest największa przy pozycjach dotykających integracji i bazy danych. Tempo zespołu mierzy się z poprzednich iteracji, czyli sumą godzin pozycji zamkniętych zgodnie z definicją ukończenia. Na tej podstawie planuje się kolejną iterację, a nie na podstawie życzeń co do terminu.

Pozycje, których nie da się oszacować, dostają najpierw krótkie badanie techniczne z ustalonym limitem czasu. Wynik badania może być prototypem albo notatką z opisem ryzyka. Dopiero po nim pozycja wraca na listę prac z oszacowaniem.

Wymagania jako lista prac i definicja ukończenia

Wymagania trafiają do listy prac, w której każda pozycja ma opis z priorytetem oraz kryterium akceptacji. Pozycja jest zamknięta wtedy, gdy spełnia definicję ukończenia uzgodnioną z zamawiającym na początku projektu. Bez takiej definicji „gotowe” znaczy co innego dla programisty i dla użytkownika.

  • Kod - zmiana przeszła przegląd i została scalona z gałęzią główną, a build kończy się powodzeniem.
  • Testy - funkcja ma testy automatyczne, a scenariusz akceptacyjny został wykonany na środowisku testowym.
  • Baza danych - skrypt migracji jest dołączony do zmiany i przeszedł próbę na kopii bazy.
  • Dokumentacja - zmiana jest opisana w notatkach do wydania, a jeśli dotyczy konfiguracji, także w instrukcji.
Monitor z otwartym oknem dokumentacji technicznej na tle rozświetlonej panoramy biurowców
Dokumentacja techniczna prowadzona razem z kodem: opis zmian i konfiguracji powstaje w tej samej iteracji

Sposób doprecyzowania zakresu przed startem opisuje tekst o analizie przedwdrożeniowej. Pozycje z listy prac powinny wynikać z tej analizy, a nie z pojedynczych rozmów z użytkownikami.

Repozytorium i przegląd kodu

Kod źródłowy leży w repozytorium z historią zmian, najczęściej w Git. Gałąź główna zawiera stan gotowy do wdrożenia, a każda funkcja powstaje na osobnej gałęzi krótkiego życia. Krótka gałąź ogranicza konflikty przy scalaniu, bo różnica względem głównej jest niewielka.

git checkout -b funkcja/uwagi-do-zamowienia
git commit -m "Dodano pole uwag do zamowienia"
git push origin funkcja/uwagi-do-zamowienia
# po przeglądzie i zielonym buildzie:
git checkout main
git merge --no-ff funkcja/uwagi-do-zamowienia
git tag v1.4.0

Znacznik na końcu oznacza wydanie. Dzięki niemu każdą wersję działającą u klienta da się odtworzyć z repozytorium, co jest warunkiem szukania błędu zgłoszonego na produkcji.

Automatyczny build po każdym scaleniu

Serwer buildów kompiluje aplikację i uruchamia testy po każdym scaleniu, a wynik jest widoczny przy zmianie. Build jest powtarzalny, bo wykonuje się w czystym środowisku według skryptu zapisanego w repozytorium, a nie na komputerze programisty. Efektem jest pakiet oznaczony numerem builda, który przechodzi kolejne środowiska bez ponownej kompilacji.

Co sprawdza przegląd kodu

Przegląd nie ma zastępować testów. Recenzent szuka rzeczy, których test nie złapie: zapytań wykonywanych w pętli, braku obsługi wyjątków, wartości wpisanych na stałe zamiast konfiguracji i zmian uprawnień. Z punktu widzenia bazy danych szczególnie ważny jest przegląd skryptów migracji, bo błąd w nich trudniej cofnąć niż błąd w kodzie aplikacji.

Ta praktyka wiąże się z tematem opisanym w tekście o programowaniu baz danych: zmiany schematu podlegają tym samym regułom przeglądu co kod.

Poziomy testów aplikacji biznesowej

Testy dzieli się według tego, jak duży fragment systemu obejmują. Im niższy poziom, tym test jest szybszy i tańszy w utrzymaniu, więc tych testów powinno być najwięcej. Testy wyższego poziomu są wolniejsze, ale sprawdzają współpracę elementów, której testy niższego poziomu nie widzą.

PoziomCo sprawdzaKto wykonujeKiedy
JednostkowyPojedynczą metodę, na przykład naliczanie kwoty bruttoProgramistaPrzy każdym buildzie
IntegracyjnyWspółpracę kodu z bazą danych lub interfejsem sąsiedniego systemuProgramista i testerPrzy każdym buildzie lub co noc
AkceptacyjnyScenariusz biznesowy od początku do końca według kryteriówUżytkownik końcowyPrzed wydaniem
RegresjiCzy zmiana nie zepsuła funkcji, które działały wcześniejZestaw automatyczny i testerPrzed każdym wydaniem

Dane testowe i środowisko testowe

Testy akceptacyjne wymagają realistycznych danych. Najprościej wziąć kopię bazy produkcyjnej, ale dane osobowe nie powinny trafić do środowiska testowego bez zanonimizowania, bo dostęp do niego ma szersze grono osób. Skrypt anonimizacji jest częścią procesu odtwarzania środowiska i uruchamia się po każdym przywróceniu kopii.

UPDATE dbo.Kontrahenci
SET Email   = CONCAT('kontrahent', Id, '@example.test'),
    Telefon = NULL;
Ilustracja bazy danych: cylindry magazynów danych, ikony raportów i chmury połączone liniami na granatowym tle
Dane środowiska testowego pochodzą z kopii bazy po anonimizacji, dzięki czemu scenariusze akceptacyjne działają na realistycznym zbiorze

Poniższy przykład pokazuje test jednostkowy w xUnit. Kwoty przechowuje w groszach jako liczby całkowite, co unika błędów zaokrągleń typowych dla liczb zmiennoprzecinkowych. Nazwy klas i metod są przykładowe.

public static class Kalkulator
{
    public static int Brutto(int nettoGrosze, int stawkaVat) =>
        (int)Math.Round(nettoGrosze * (100 + stawkaVat) / 100.0,
                        MidpointRounding.AwayFromZero);
}

public class KalkulatorTests
{
    [Theory]
    [InlineData(10000, 23, 12300)]
    [InlineData(1999, 23, 2459)]
    public void Brutto_zaokragla_do_groszy(int netto, int stawka, int oczekiwane)
    {
        Assert.Equal(oczekiwane, Kalkulator.Brutto(netto, stawka));
    }
}

Wprowadzenie do pisania takich testów w środowisku .NET zawiera dokumentacja Microsoft Learn o testach jednostkowych w .NET. Testy integracyjne z interfejsami sąsiednich systemów omawia tekst o integrowaniu aplikacji webowych. Testy akceptacyjne pisze się z użytkownikami, ponieważ to oni wiedzą, jaki wynik jest poprawny dla danego dokumentu.

Reguła: błąd zgłoszony z produkcji dostaje najpierw test, który go odtwarza, a dopiero potem poprawkę.

Test odtwarzający błąd zostaje w zestawie regresji. Po roku prowadzenia takiej praktyki zestaw zawiera historię wszystkich błędów, które kiedykolwiek się pojawiły, i pilnuje, żeby nie wróciły.

Testy wydajności i bezpieczeństwa

Testy obciążeniowe sprawdzają, jak aplikacja zachowuje się przy liczbie jednoczesnych użytkowników zbliżonej do rzeczywistej. Mierzy się czas odpowiedzi ekranów i zapytań, a wolne zapytania analizuje pod kątem indeksów i planu wykonania. Do porównania zapytań między wersjami służy Query Store, opisany w dokumentacji Microsoft Learn.

Testy bezpieczeństwa obejmują przede wszystkim kontrolę uprawnień, czyli sprawdzenie, czy użytkownik bez roli nie otworzy cudzego dokumentu przez zmianę adresu. Drugi obszar to ochrona przed wstrzyknięciem SQL. Zapytania z parametrami zamiast sklejania tekstu rozwiązują większość problemów.

using var cmd = new SqlCommand(
    "SELECT Id, Nazwa FROM dbo.Kontrahenci WHERE NIP = @nip", conn);
cmd.Parameters.Add("@nip", SqlDbType.NVarChar, 20).Value = nip;

Każde miejsce, w którym aplikacja buduje zapytanie z tekstu podanego przez użytkownika, wychwytuje się podczas przeglądu kodu. Reguła jest prosta: wartość z formularza trafia do parametru, a nie do treści zapytania.

Wersjonowanie aplikacji i bazy danych

Numer wersji informuje, jakiego rodzaju zmiana zaszła. Najczęściej stosuje się schemat trzech liczb rozdzielonych kropkami, opisany w specyfikacji wersjonowania semantycznego. Pierwsza liczba rośnie przy zmianie niezgodnej wstecz, druga przy nowej funkcji, trzecia przy samej poprawce błędu.

Rodzaj zmianyZmienia sięPrzykład
Poprawka błędu bez zmiany działaniaTrzecia liczba1.4.0 na 1.4.1
Nowa funkcja zgodna wsteczDruga liczba1.4.1 na 1.5.0
Zmiana niezgodna z poprzednim interfejsemPierwsza liczba1.5.0 na 2.0.0

Wersja schematu bazy danych

Baza danych ma własną historię zmian, niezależną od numeru aplikacji. Każdy skrypt migracji zapisuje w tabeli wersji, że został wykonany, dzięki czemu ten sam skrypt uruchomiony drugi raz niczego nie zmienia. Poniższy przykład w T-SQL dodaje pole i odnotowuje wersję schematu w jednej transakcji, a nazwy tabel są przykładowe.

CREATE TABLE dbo.WersjaSchematu (
    Wersja   INT           NOT NULL PRIMARY KEY,
    Opis     NVARCHAR(200) NOT NULL,
    Wdrozono DATETIME2(0)  NOT NULL DEFAULT SYSDATETIME()
);

SET XACT_ABORT ON;
IF NOT EXISTS (SELECT 1 FROM dbo.WersjaSchematu WHERE Wersja = 12)
BEGIN
    BEGIN TRANSACTION;
    ALTER TABLE dbo.Zamowienia ADD Uwagi NVARCHAR(500) NULL;
    INSERT dbo.WersjaSchematu (Wersja, Opis)
    VALUES (12, N'Uwagi do zamówienia');
    COMMIT TRANSACTION;
END;

Nowa kolumna dopuszcza wartości NULL, więc stara wersja aplikacji nadal działa na zmienionej bazie. Ten wzorzec pozwala wdrażać bazę przed aplikacją i zmniejsza ryzyko przerwy w pracy użytkowników. Zmiany usuwające kolumny wprowadza się dopiero po wycofaniu wszystkich wersji, które ich używają.

Środowiska i wydania

Ten sam pakiet wdraża się kolejno na środowisko testowe i produkcyjne. Zmienia się tylko konfiguracja, taka jak łańcuch połączenia i adresy integracji. Dzięki temu na produkcji działa dokładnie to, co przeszło testy akceptacyjne, a nie ponowny build z innego stanu repozytorium.

Do każdego wydania dołącza się notatki opisujące nowe funkcje i poprawione błędy oraz wymagane kroki konfiguracyjne. Użytkownicy dostają z nich informację, co się zmieniło, a dział wsparcia dostaje listę rzeczy do sprawdzenia po wdrożeniu. Praktykę tę wspiera opis z tekstu o aplikacji biznesowej jako module systemu IT.

Technik w granatowej kurtce i czapce przy monitorze z dokumentem w ręku w hali z rurociągami
Odbiór wdrożenia na miejscu: porównanie wyniku scenariusza akceptacyjnego z dokumentem źródłowym

Wycofanie wydania

Wycofanie aplikacji jest proste, bo wystarczy wdrożyć poprzedni pakiet. Wycofanie bazy jest trudne, jeśli w międzyczasie zapisano nowe dane. Z tego powodu migracje planuje się jako zmiany zgodne wstecz, a przed wydaniem wykonuje się kopię bazy z potwierdzoną możliwością odtworzenia.

  • Zmiana zgodna wstecz - stara wersja aplikacji działa na nowym schemacie, więc wycofanie ogranicza się do pakietu aplikacji.
  • Zmiana niezgodna wstecz - wymaga osobnego planu, kopii bazy przed wydaniem i decyzji, do kiedy wycofanie jest jeszcze możliwe.

Poprawki po wdrożeniu i utrzymanie wersji

Po wdrożeniu praca nie kończy się, bo użytkownicy zgłaszają uwagi. Zmieniają się także przepisy i procesy. Poprawkę pilną, na przykład błąd blokujący wystawianie dokumentów, wprowadza się na osobnej gałęzi wydania i dostarcza jako zwiększenie trzeciej liczby. Zmiany zwykłe czekają na najbliższe planowe wydanie.

Rodzaj zgłoszeniaŚcieżka w repozytoriumTermin wdrożenia
Błąd blokujący pracęGałąź wydania i zwiększenie trzeciej liczbyPoza cyklem, po teście regresji
Błąd zwykłyGałąź funkcjonalna i przegląd koduNajbliższe planowe wydanie
Zmiana wymagańNowa pozycja na liście pracIteracja wskazana przez zamawiającego

Koszty tego okresu opisuje tekst o tworzeniu programów dla firm, a zasady doboru wykonawcy i zakres oferty przedstawia strona producenta aplikacji biznesowych. Pełną listę zagadnień programowania takich aplikacji zawiera artykuł o programowaniu aplikacji biznesowych.

Przy wielu wdrożeniach tej samej aplikacji pojawia się pytanie, ile wersji utrzymuje się równolegle. Rozsądna zasada mówi, że poprawki bezpieczeństwa trafiają do wersji aktualnej i poprzedniej, a starsze wymagają aktualizacji. Zasadę spisuje się w umowie, aby zamawiający wiedział, kiedy wersja wychodzi z okresu wsparcia.

Reguła: wersja poza okresem wsparcia nie dostaje poprawek bezpieczeństwa, więc umowa podaje datę końca wsparcia dla każdego wydania.

Aplikacje webowe udostępniane przez przeglądarkę wymagają jeszcze planu wsparcia przeglądarek. Wykonawca ustala z zamawiającym listę wersji, na których program jest testowany, a każde odstępstwo zapisuje w notatkach do wydania. Więcej o projektowaniu warstwy interfejsu jest w tekście o projektowaniu aplikacji webowych, a ofertę na zamówienie można sprawdzić na stronie usług SoftwareStudio.