PrzedsprzedażAkademia AI w testach i jakości oprogramowania: 1 350 zł zamiast 1 750 zł netto do 30 listopada

Startuje przedsprzedaż Akademii AI w testach i jakości oprogramowania: 5 tygodni z Tomaszem Stelmachem, start 11 stycznia 2027. Wyjątkowa cena 1 350 zł netto zamiast 1 750 zł tylko do 30 listopada.

zostało 25 dniZobaczcie program i zapiszcie się
17. Hermetyzacja w Javie: modyfikatory dostępu, rekordy i Page Object
Czas czytania: 12 minut

Hermetyzacja w Javie (enkapsulacja) to zasada, według której obiekt sam pilnuje swoich danych: pola są prywatne, a zmienić je można tylko przez metody, które sprawdzają reguły. Dzięki temu nikt z zewnątrz nie wpisze do konta ujemnego salda ani do testu lokatora, który zna tylko jedna klasa.

To siedemnasta lekcja kursu Javy dla testerów. Pokazujemy w niej cztery modyfikatory dostępu w jednej tabeli, klasę z prywatnymi polami i walidacją, pułapkę getterów zwracających listę, rekordy z Javy 16, które robią połowę tej pracy za Was, oraz Page Object z prywatnymi lokatorami. Cały kod skompilowaliśmy poleceniem javac --release 25, a wyniki działania wpisaliśmy w komentarzach.

Czym jest hermetyzacja w Javie?

Hermetyzacja w Javie polega na ukryciu stanu obiektu przed resztą programu i udostępnieniu tylko tych operacji, które są bezpieczne. Klasa decyduje, co widać na zewnątrz, a co jest jej prywatną sprawą. Narzędziem do tego są modyfikatory dostępu: private, protected, public i dostęp domyślny, nazywany pakietowym.

Hermetyzacja to jeden z czterech filarów programowania obiektowego, obok abstrakcji, dziedziczenia i polimorfizmu. Ogólny obraz wszystkich czterech, z różnicą między hermetyzacją a abstrakcją, znajdziecie w lekcji 14. Programowanie obiektowe: klasy i obiekty. Tutaj schodzimy poziom niżej, do praktyki: jak pisać klasy, które bronią się same.

Po co to testerowi? Bo framework testów to też kod, który ktoś utrzymuje przez lata. Z naszego doświadczenia przy wdrożeniach automatyzacji, a w Quality Island napisaliśmy ponad 450 000 testów automatycznych, najdroższe w utrzymaniu nie są trudne testy, tylko te, w których każda klasa może zmienić wszystko w każdej innej klasie.

Hermetyzacja w Javie w liczbach

4

poziomy dostępu do pól i metod

16

wersja Javy, w której rekordy weszły na stałe

2030

do września wsparcie Premier dla Javy 25 LTS

450 000+

testów automatycznych napisanych w Quality Island

Źródła: Java Language Specification SE 25, rozdział 6.6; OpenJDK JEP 395; Oracle Java SE Support Roadmap 2026; dane własne Quality Island 2026.

Jakie są modyfikatory dostępu w Javie?

Modyfikatory dostępu w Javie są cztery, a specyfikacja języka Java SE 25 (rozdział 6.6) opisuje dokładnie, skąd wolno sięgnąć po każdy element. Najłatwiej zapamiętać je w tabeli, od najwęższego do najszerszego. Uwaga na pułapkę, o którą potykają się nawet osoby po kilku latach pracy: protected daje więcej niż dostęp domyślny, bo poza podklasami obejmuje też cały pakiet.

Modyfikator Ta sama klasa Ten sam pakiet Podklasa w innym pakiecie Reszta świata
private tak nie nie nie
brak modyfikatora (pakietowy) tak tak nie nie
protected tak tak tak nie
public tak tak tak tak

Źródło: Oracle, The Java Tutorials, Controlling Access to Members of a Class; Java Language Specification SE 25, 2025, rozdział 6.6.

Praktyczna reguła, którą stosujemy w naszych projektach: zaczynajcie od private i otwierajcie dostęp dopiero wtedy, gdy macie konkretny powód. Zmiana z prywatnego na publiczny jest łatwa. Zmiana w drugą stronę, gdy z publicznego pola korzysta już 300 testów, oznacza popołudnie z wyszukiwaniem i poprawianiem.

Dostęp pakietowy ma w testach jedno bardzo przyjemne zastosowanie. Dokumentacja JUnit mówi wprost, że klasy i metody testowe nie muszą być publiczne, wystarczy, że nie są prywatne, i zaleca pomijanie słowa public. Dlatego w przykładzie na końcu tej lekcji klasa LogowanieTest nie ma żadnego modyfikatora.

Co się psuje, gdy pola są publiczne?

Publiczne pole to umowa z całym światem: każdy może wpisać do niego, co chce, i nikt tego nie sprawdzi. Poniżej klasa konta w dwóch wersjach. Pierwsza wpuszcza wszystko, druga broni swojego stanu.

KontoPubliczne.java, wersja do unikania

package pl.qi.dane;

public class KontoPubliczne {
    public String email;
    public int saldo;
}

Konto.java, Java 25

package pl.qi.dane;

public class Konto {
    private final String email;
    private int saldo;

    public Konto(String email) {
        if (email == null || !email.contains("@")) {
            throw new IllegalArgumentException("Niepoprawny e-mail: " + email);
        }
        this.email = email;
    }

    public String getEmail() {
        return email;
    }

    public int getSaldo() {
        return saldo;
    }

    public void wplac(int kwota) {
        if (kwota <= 0) {
            throw new IllegalArgumentException("Kwota musi być dodatnia: " + kwota);
        }
        saldo += kwota;
    }
}

KontoDemo.java, wynik w komentarzach

package pl.qi.dane;

public class KontoDemo {
    public static void main(String[] args) {
        KontoPubliczne zle = new KontoPubliczne();
        zle.email = "bez-malpy";
        zle.saldo = -500;
        System.out.println(zle.email + " " + zle.saldo); // bez-malpy -500

        Konto dobre = new Konto("anna@example.com");
        dobre.wplac(200);
        System.out.println(dobre.getSaldo()); // 200
        try {
            dobre.wplac(-500);
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage()); // Kwota musi być dodatnia: -500
        }
    }
}

Zwróćcie uwagę na trzy rzeczy. Pole email jest private final, więc ustawia je tylko konstruktor i tylko raz. Saldo nie ma settera, ma metodę wplac, która odrzuca kwoty ujemne i zerowe. A obiekt z błędnym e-mailem w ogóle nie powstanie, bo konstruktor rzuci wyjątek. Szczegóły o tym, jak konstruktor pilnuje poprawności obiektu, są w lekcji 15. Konstruktory.

Dla testera to dobra wiadomość podwójnie. Po pierwsze, klasa z regułami w środku sama mówi, co trzeba przetestować: wartości graniczne kwoty, e-mail bez znaku @, wartość null. Po drugie, test nie musi sprawdzać, czy ktoś gdzieś nie ustawił salda na minus 500. Bezpośredni zapis do prywatnego pola zatrzyma kompilator, a ujemną kwotę podaną przez metodę odrzuci walidacja w czasie działania programu, i to ją sprawdzacie testem. Publiczne pole to jak zostawienie kluczy w drzwiach z karteczką „proszę nie wchodzić”: działa dokładnie do pierwszego gościa.

Gettery i settery: kiedy setter, a kiedy metoda z regułą?

Getter (akcesor) zwraca wartość prywatnego pola, setter (mutator) ją zmienia. Zgodnie z konwencją JavaBeans nazywają się getEmail() i setEmail(), a dla wartości logicznych isAktywny(). Sama para getter i setter bez żadnej logiki nie daje jednak hermetyzacji, tylko jej pozór: pole jest prywatne, ale każdy może je zmienić na dowolną wartość.

Dlatego zanim dopiszecie setter, zadajcie sobie trzy pytania:

  1. Czy ta wartość w ogóle powinna się zmieniać po utworzeniu obiektu? Jeśli nie, pole jest final, a settera nie ma. Tak jest z e-mailem w klasie Konto.
  2. Czy zmiana ma regułę biznesową? Jeśli tak, zamiast setSaldo(int) piszecie metodę nazwaną od czynności: wplac, wyplac, zablokuj. Reguła mieszka wtedy w jednym miejscu.
  3. Czy getter nie zdradza zbyt wiele? Jeśli zwraca obiekt modyfikowalny, na przykład listę albo datę typu java.util.Date, ktoś może zmienić stan obiektu bez pytania. O tym w następnej sekcji.

Ten sposób myślenia przyda się Wam od razu w testach. Obiekt z danymi testowymi, który ma tylko gettery i jest tworzony w jednym miejscu, nie zmieni się przypadkiem między krokiem 3 a krokiem 7 testu. Z naszego doświadczenia ze szkoleń, przez które przeszło ponad 2 500 specjalistów, właśnie dane testowe zmieniane w połowie scenariusza są jedną z najczęstszych przyczyn testów, które „czasem przechodzą”.

Jak getter może złamać hermetyzację?

Getter łamie hermetyzację, gdy zwraca referencję do wewnętrznej, modyfikowalnej kolekcji. Pole jest prywatne, ale wywołujący dostaje do ręki ten sam obiekt listy i może go wyczyścić. Naprawa to zwrócenie kopii tylko do odczytu, na przykład przez List.copyOf, dostępne od Javy 10.

Koszyk.java

package pl.qi.dane;

import java.util.ArrayList;
import java.util.List;

public class Koszyk {
    private final List<String> produkty = new ArrayList<>();

    public void dodaj(String produkt) {
        produkty.add(produkt);
    }

    // Przeciek: zwracamy oryginalną listę, każdy może ją zmienić
    public List<String> getProduktyZPrzeciekiem() {
        return produkty;
    }

    // Poprawnie: niemodyfikowalna kopia
    public List<String> getProdukty() {
        return List.copyOf(produkty);
    }
}

KoszykDemo.java, wynik w komentarzach

package pl.qi.dane;

public class KoszykDemo {
    public static void main(String[] args) {
        Koszyk koszyk = new Koszyk();
        koszyk.dodaj("Kubek");
        koszyk.getProduktyZPrzeciekiem().clear();
        System.out.println(koszyk.getProdukty().size()); // 0, koszyk wyczyszczony z zewnątrz

        koszyk.dodaj("Kubek");
        try {
            koszyk.getProdukty().clear();
        } catch (UnsupportedOperationException e) {
            System.out.println("Kopia jest tylko do odczytu"); // ten komunikat zobaczycie
        }
        System.out.println(koszyk.getProdukty().size()); // 1
    }
}

Dokumentacja klasy List w Javie 25 opisuje, że listy z List.of i List.copyOf są niemodyfikowalne i każda próba zmiany kończy się wyjątkiem UnsupportedOperationException. To lepsze zachowanie niż ciche przyjęcie zmiany: błąd wychodzi od razu, w miejscu, które go spowodowało.

Wbrew pozorom

W Javie private chroni pole przed innymi klasami, a nie przed innymi obiektami tej samej klasy. Specyfikacja Java SE 25 (rozdział 6.6.1) pozwala na dostęp do składowej prywatnej z dowolnego miejsca w ciele klasy najwyższego poziomu, która ją deklaruje. Gdybyście dopisali do klasy Konto metodę equals, mogłaby ona bez gettera odczytać inne.saldo z drugiego obiektu. Hermetyzacja w Javie działa na poziomie klasy, nie pojedynczego egzemplarza.

Czy rekordy w Javie zastępują gettery?

Dla klas, które tylko przenoszą dane, tak. Rekord (record) wszedł do Javy na stałe w wersji 16, zgodnie z dokumentem OpenJDK JEP 395. Kompilator sam generuje dla każdego składnika pole private final, publiczny akcesor o tej samej nazwie, konstruktor kanoniczny oraz metody equals, hashCode i toString. To hermetyzacja w jednej linijce, bez kilkudziesięciu linii getterów.

Dwie rzeczy warto wiedzieć, zanim zamienicie wszystkie klasy na rekordy. Akcesory nie mają przedrostka get: piszecie uzytkownik.login(), a nie getLogin(). A pola są final, ale obiekt, na który wskazują, już niekoniecznie. Jeśli składnikiem jest lista, zróbcie jej kopię w konstruktorze kompaktowym, tak jak poniżej.

Uzytkownik.java, rekord z walidacją

package pl.qi.dane;

import java.util.List;

public record Uzytkownik(String login, String haslo, List<String> role) {
    public Uzytkownik {
        if (login == null || login.isBlank()) {
            throw new IllegalArgumentException("Login nie może być pusty");
        }
        role = List.copyOf(role);
    }

    public static Uzytkownik administrator() {
        return new Uzytkownik("admin.test", "Haslo123!", List.of("ADMIN"));
    }
}

UzytkownikDemo.java, wynik w komentarzach

package pl.qi.dane;

import java.util.ArrayList;
import java.util.List;

public class UzytkownikDemo {
    public static void main(String[] args) {
        List<String> role = new ArrayList<>(List.of("TESTER"));
        Uzytkownik u = new Uzytkownik("anna", "Haslo123!", role);
        role.add("ADMIN");
        System.out.println(u.role());  // [TESTER], zmiana listy z zewnątrz nie przeszła
        System.out.println(u.login()); // anna, akcesor bez przedrostka get
        System.out.println(u.equals(new Uzytkownik("anna", "Haslo123!", List.of("TESTER")))); // true
    }
}

Jedna pułapka przy rekordach z danymi logowania: wygenerowany toString wypisuje wszystkie składniki, także hasło. System.out.println(Uzytkownik.administrator()) pokazuje w logu Uzytkownik[login=admin.test, haslo=Haslo123!, role=[ADMIN]], co sprawdziliśmy w JDK 25. Jeśli taki rekord trafia do logów albo raportów z testów, nadpiszcie w nim toString i zamaskujcie hasło, na przykład zwracając haslo=***.

Sytuacja Klasa z polami private Rekord
Dane testowe (użytkownik, produkt, adres) działa, ale dużo kodu najlepszy wybór
Odpowiedź z API w testach REST potrzebna, gdy biblioteka wymaga konstruktora bez argumentów dobry wybór, gdy biblioteka obsługuje rekordy
Stan zmieniający się w czasie (saldo, koszyk) najlepszy wybór, z metodami z regułami nie, rekord jest niezmienny
Page Object tak, z prywatnymi lokatorami nie, akcesor wystawiłby sterownik na zewnątrz

Źródło: OpenJDK, JEP 395: Records, 2021; Oracle, Java SE 25 Language Guide, Record Classes; zestawienie Quality Island.

Hermetyzacja w testach: Page Object z prywatnymi lokatorami

Page Object to klasa, która reprezentuje stronę albo jej fragment i ukrywa przed testami szczegóły HTML. Dokumentacja Selenium opisuje to wprost: Page Object ma być jedynym miejscem, które zna strukturę strony, nie powinien wystawiać swoich wnętrzności i nie powinien robić asercji, bo to zadanie testu. Martin Fowler w tekście „PageObject” z 2013 roku ujmuje to podobnie: obiekt strony udostępnia usługi, z których korzysta test, a nie elementy strony.

To jest hermetyzacja w czystej postaci. Lokatory są private static final, metoda pomocnicza wpiszDane jest prywatna, a konstruktor PanelKlienta ma dostęp pakietowy, więc panel może utworzyć tylko klasa z tego samego pakietu, po udanym logowaniu. Kod skompilowaliśmy z Selenium 4.

StronaLogowania.java, Selenium 4

package pl.qi.strony;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class StronaLogowania {
    private static final By POLE_LOGIN = By.id("username");
    private static final By POLE_HASLO = By.id("password");
    private static final By PRZYCISK_ZALOGUJ = By.cssSelector("button[type='submit']");
    private static final By KOMUNIKAT_BLEDU = By.cssSelector("[data-testid='login-error']");

    private final WebDriver driver;

    public StronaLogowania(WebDriver driver) {
        this.driver = driver;
    }

    public PanelKlienta zalogujJako(String login, String haslo) {
        wpiszDane(login, haslo);
        return new PanelKlienta(driver);
    }

    public StronaLogowania zalogujZBledem(String login, String haslo) {
        wpiszDane(login, haslo);
        return this;
    }

    public String komunikatBledu() {
        return driver.findElement(KOMUNIKAT_BLEDU).getText();
    }

    private void wpiszDane(String login, String haslo) {
        driver.findElement(POLE_LOGIN).clear();
        driver.findElement(POLE_LOGIN).sendKeys(login);
        driver.findElement(POLE_HASLO).sendKeys(haslo);
        driver.findElement(PRZYCISK_ZALOGUJ).click();
    }
}

PanelKlienta.java

package pl.qi.strony;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class PanelKlienta {
    private static final By POWITANIE = By.cssSelector("[data-testid='welcome']");

    private final WebDriver driver;

    PanelKlienta(WebDriver driver) {
        this.driver = driver;
    }

    public String powitanie() {
        return driver.findElement(POWITANIE).getText();
    }
}

LogowanieTest.java, JUnit Jupiter

package pl.qi.testy;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import pl.qi.dane.Uzytkownik;
import pl.qi.strony.StronaLogowania;

class LogowanieTest {
    private WebDriver driver;

    @BeforeEach
    void otworzStrone() {
        driver = new ChromeDriver(); // Selenium Manager sam pobierze sterownik
        driver.get("https://sklep-demo.example/logowanie");
    }

    @Test
    void administratorWidziPowitanie() {
        Uzytkownik admin = Uzytkownik.administrator();
        String powitanie = new StronaLogowania(driver)
                .zalogujJako(admin.login(), admin.haslo())
                .powitanie();
        assertEquals("Witaj, admin.test", powitanie);
    }

    @Test
    void zleHasloPokazujeKomunikat() {
        String komunikat = new StronaLogowania(driver)
                .zalogujZBledem("admin.test", "zle-haslo")
                .komunikatBledu();
        assertEquals("Nieprawidłowy login lub hasło", komunikat);
    }

    @AfterEach
    void zamknij() {
        driver.quit();
    }
}

Zobaczcie, czego w teście nie ma: ani jednego By.id, ani jednego selektora CSS. Gdy zespół frontendu zmieni identyfikator pola z username na login, poprawiacie jedną linijkę w StronaLogowania, a nie 40 testów. W naszych projektach automatyzacji to właśnie ta jedna decyzja, prywatne lokatory w obiektach stron, najbardziej obniża koszt utrzymania testów UI. Więcej wzorców, które działają razem z Page Object, opisujemy w artykule o wzorcach projektowych w automatyzacji testów Selenium WebDriver, a dobór samych lokatorów w tekście selektory w Selenium.

Jeśli chcecie zbudować taki framework na szkoleniu z trenerem, zobaczcie szkolenie z automatyzacji testów w Selenium. Gdy wolicie, żeby zrobił to za Was zespół z doświadczeniem, zakres i ceny pokazuje strona automatyzacji testów UI.

Jakie błędy hermetyzacji widzimy najczęściej?

Przeglądając frameworki testów u klientów, widzimy wciąż te same pięć błędów. Każdy ma prostą poprawkę:

  1. Publiczne pola w klasach z danymi testowymi. Poprawka: rekord albo pola private final z konstruktorem.
  2. Getter i setter do każdego pola, generowany odruchowo przez IDE. Poprawka: setter tylko tam, gdzie zmiana ma sens, a zamiast niego metoda nazwana od czynności.
  3. Getter zwracający wewnętrzną listę. Poprawka: List.copyOf albo kopia w konstruktorze rekordu.
  4. Lokatory publiczne albo w klasie testowej. Poprawka: private static final By w obiekcie strony.
  5. Asercje w Page Object. Poprawka: obiekt strony zwraca dane, test je sprawdza, tak jak zaleca dokumentacja Selenium.

Pozostałe antywzorce, które psują testy szybciej niż zmiany w aplikacji, zebrała Strefa QA w tekście o najczęstszych antywzorcach w testowaniu oprogramowania. W kolejnej lekcji, 18. Dziedziczenie, zobaczycie, dlaczego protected trzeba używać ostrożnie. Jeśli wolicie przerobić cały materiał z trenerem, mamy szkolenia Programowanie Java dla testerów i Programowanie obiektowe w Java, a osobom, które zaczynają od zera, polecamy wprowadzenie do programowania od podstaw w Javie.

Co zabrać z tego artykułu

01Hermetyzacja w Javie to prywatny stan i publiczne operacje, które pilnują reguł. Sama para getter i setter jej nie daje.

02Cztery modyfikatory dostępu od najwęższego: private, pakietowy, protected, public. Protected obejmuje też cały pakiet.

03Getter zwracający wewnętrzną listę łamie hermetyzację. Zwracajcie List.copyOf.

04Rekordy (od Javy 16) to najlepszy wybór na dane testowe. Listy kopiujcie w konstruktorze kompaktowym.

05W Page Object lokatory są prywatne, asercje zostają w teście. Zmiana selektora to wtedy jedna linijka.

Wasze testy UI pękają przy każdej zmianie w interfejsie? Zbudujemy albo uporządkujemy framework z obiektami stron, które chronią lokatory i skracają utrzymanie.

Zobaczcie zakres i cennik

Poprzednia lekcja16. FunkcjeSpis kursuWszystkie lekcje kursu JavyNastępna lekcja18. Dziedziczenie

Powiązane na blogu Quality Island

Pełna lista źródeł

Co o tym sądzisz?

Dodaj komentarz

Bądź na bieżąco
Strefa QA, portal z eksperckimi artykułami o jakości oprogramowania
Bądź na bieżąco
Szkolenie Specjalista ds. AI w organizacji: program przekwalifikowujący
Specjalista ds. AI w organizacji
Specjalista ds. AI w organizacji: program przekwalifikowujący

4 400,00 zł

03.02.27, 14.04.27, 02.06.27
5 dni
Szkolenie Inżynier AI: program przekwalifikowujący dla specjalistów IT
Inżynier AI
Inżynier AI: program przekwalifikowujący dla specjalistów IT

6 900,00 zł

12.01.27, 16.03.27
8 tygodni
KURS ONLINE: PAQT Foundation, AI w pracy testera, przygotowanie do egzaminu

590,00 zł

Popularne artykuły
Język Gherkin: co to jest i jak go używać w testowaniu oprogramowania, grafika Quality Island
Język Gherkin: co to jest i jak go używać w testowaniu oprogramowania
Smoke test: co to jest i czym różni się od sanity testu, grafika Quality Island
Smoke test: co to jest, kiedy go uruchamiać i czym różni się od sanity testu
Jak zostać testerem oprogramowania: ścieżka krok po kroku, grafika Quality Island
Jak zostać testerem oprogramowania: ścieżka krok po kroku bez dyplomu informatyka
Najnowsze artykuły
Audyt WCAG 2.2: przebieg, ceny w zł i szablon zapytania ofertowego, okładka artykułu Quality Island
Audyt WCAG 2.2: przebieg, ceny w zł i szablon zapytania ofertowego
Tester manualny: co robi, ile zarabia i checklista kompetencji na pierwszą rozmowę, grafika Quality Island
Tester manualny: co robi, ile zarabia w 2026 i checklista kompetencji na rozmowę
Art. 4 AI Act: kompetencje w zakresie AI w zespole IT i QA, checklista i rejestr szkoleń AI
Art. 4 AI Act po Digital Omnibus: kompetencje w zakresie AI w zespole IT i QA, checklista i rejestr szkoleń
Popularne kategorie

Grupa marek jakości oprogramowania

Jakość oprogramowania w Polsce ma swój adres

Quality Island