Unitтестирање у мобилном развоју: шта је, методе и фрејморкови

Аутор: IT Sectr Објављено: 2026-04-06 Време читања: 9 мин

Unit тестирање је метод провере софтвера при којем се тестира исправност појединачних модула или функција кода у изолацији од остатка система. Према Martin Fowler, 2026, unit тестови су темељ CI/CD и рефакторисања, обезбеђујући брзу повратну информацију о исправности кода. Модуларно тестирање помаже у откривању грешака у раним фазама развоја, смањујући трошкове њиховог исправљања десетинама пута.

Главно

  • Unit тест — провера једног модула (функције, методе, класе) у изолацији од спољних зависности
  • FIRST принципи — Fast, Isolated, Repeatable, Self-validating, Timely — основа квалитетних тестова
  • Мокови и стабови — заменици спољних зависности (база података, API, систем датотека), обезбеђујући изолацију теста
  • TDD (Test-Driven Development) — методологија развоја кроз тестирање: црвено-зелено-рефакторисање
  • Пирамида тестова — unit тестови заузимају 70% пирамиде, обезбеђујући брзу повратну информацију на сваком комиту

Шта је unit тестирање?

Unit тестирање је процес провере појединачних јединица (unit) изворног кода — функција, метода, класа — у изолацији од остатка програма. Сваки тест покреће одређени сценарио коришћења модула и проверава да ли резултат одговара очекиваном. Unit тестови се пишу на истом програмском језику као и основни код и извршавају се аутоматски у развојном окружењу или у CI/CD пајплајну. За разлику од интеграционих тестова, unit тестови не интерагују са стварним базама података, системом датотека или мрежним сервисима.

Зашто су потребни unit тестови?

Основни циљ је брза повратна информација о исправности кода након промена. Ако програмер рефакторише метод, сет unit тестова потврђује да понашање није нарушено. Према Google Testing Blog (2025), пројекти са покривеношћу unit тестовима изнад 60% имају 2,5 пута мање инцидената у продукцији. Додатне предности: документација кода (тестови показују како користити API), поједностављење рефакторисања (можете мењати имплементацију задржавајући понашање) и брза дијагностика регресија.

Шта се сматра unit тестом?

Није сваки аутоматски тест unit тест. Критеријуми: тестира се један модул (класа или функција), спољне зависности замењене су моковима или стабовима, тест се извршава у милисекундама, не захтева подизање сервера или базе података. Тест који приступа стварној бази података је интеграциони тест. Тест који отвара прегледач је E2E тест. Разумевање граница између врста тестова је важно за правилну расподелу напора у пирамиди тестирања.

FIRST принципи и AAA структура

Квалитетни unit тестови следе FIRST принципе које је формулисао Robert C. Martin. Сваки тест треба да буде Fast (брз — милисекунде), Isolated (изолован — не зависи од других тестова), Repeatable (поновљив — исти резултат на свакој машини), Self-validating (самопроверљив — резултат „passed” или „failed”, без ручне провере) и Timely (благовремен — написан пре или истовремено са кодом). Кршење било ког принципа смањује вредност теста.

AAA структура (Arrange-Act-Assert)

Стандардни шаблон за писање unit тестова. Arrange — припрема података и зависности: креирање објеката, подешавање мокова, задавање улазних параметара. Act — извршавање тестиране радње: позивање методе или функције. Assert — провера резултата: поређење стварне вредности са очекиваном. Подела на три блока чини тест читљивим и разумљивим. Ако Assert блок захтева сложену логику, тест вероватно проверава превише тога одједном.

kotlin
// Пример unitтеста по AAA шаблону на Kotlin са JUnit 5
class CalculatorTest {

    private lateinit var calculator: Calculator

    @BeforeEach
    fun setUp() {
        // ARRANGE — креирамо тестирани објекат
        calculator = Calculator()
    }

    @Test
    fun addition_shouldReturnCorrectSum() {
        // ACT — извршавамо радњу
        val result = calculator.add(2, 3)

        // ASSERT — проверавамо резултат
        Assertions.assertEquals(5, result)
    }
}

Именовање тестова

Име теста треба да опише шта се проверава и који је очекивани резултат. Формат: [methodName]_[scenario]_[expectedResult]. Пример: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Добро име теста замењује коментар и при паду одмах указује која функционалност је нарушена. Избегавајте имена попут test1, checkSomething или verify — она не носе информацију и отежавају дијагностику.

Мокови, стабови и фејкови: шта и када користити

За изолацију тестираног модула од спољних зависности користе се тест дублери (test doubles). Главни типови: мокови (mocks) — проверавају да ли је одређени метод позван са одговарајућим параметрима; стабови (stubs) — враћају задате вредности при позиву методе; фејкови (fakes) — поједностављена имплементација стварне компоненте (нпр. InMemoryUserRepository уместо UserRepository који ради са базом података). Избор зависи од тога шта треба проверити: стање (стаб) или интеракцију (мок).

ДублерШта провераваПример
МокПозив методе са исправним параметримаuserRepository.save(user) позван тачно 1 пут
СтабПовратна вредностrepository.findById(1) враћа User(id=1, name=„Test”)
ФејкЛогика кроз поједностављену имплементацијуInMemoryMapUserRepository са HashMap уместо базе података
ШпијунДелимично моковање стварног објектаspy(repo).when(findById).thenReturn(user)

Mockito: пример моковања у Java/Kotlin

Mockito је најпопуларнији фрејмворк за моковање у Java и Kotlin. Омогућава креирање мокова преко mock(), подешавање повратних вредности преко when().thenReturn() и проверу позива преко verify(). Модерне верзије Mockito (5.x) подржавају статичке мокове (mockStatic) и поједностављену синтаксу преко BDDMockito (given-willReturn). Важно правило: не мокујте оно што није ваше — не креирајте мокове за вредносне објекте и стандардне библиотеке.

kotlin
// Пример unitтеста са Mockito на Kotlin
class OrderServiceTest {

    @Mock
    private lateinit var paymentGateway: PaymentGateway

    @Mock
    private lateinit var userRepository: UserRepository

    private lateinit var orderService: OrderService

    @BeforeEach
    fun init() {
        MockitoAnnotations.openMocks(this)
        orderService = OrderService(paymentGateway, userRepository)
    }

    @Test
    fun processOrder_whenPaymentFails_shouldThrowException() {
        // given
        val user = User(id = 1, balance = 100.0)
        val order = Order(amount = 200.0)
        Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
        Mockito.`when`(userRepository.findById(1)).thenReturn(user)

        // when & then
        assert Throws<PaymentException> {
            orderService.processOrder(user.id, order)
        }

        // verify
        Mockito.verify(paymentGateway).charge(any())
    }
}

TDD: развој кроз тестирање

TDD (Test-Driven Development) — методологија у којој се тест пише пре имплементације кода. Циклус „Red-Green-Refactor”: напишите тест који пада (Red), напишите минимални код за пролазак теста (Green), побољшајте код без промене понашања (Refactor). TDD гарантује да је сав код покривен тестовима (coverage = 100% за написану функционалност) и да је код тестибилан — ако је код тешко тестирати, то значи да архитектура захтева побољшање.

Предности TDD

Према истраживању IBM (2006-2026, лонгитудинална студија), тимови који користе TDD чине 40-80% мање дефеката у продукцији у поређењу са тимовима који пишу тестове након кода. TDD такође побољшава архитектуру: програмер мора да размишља о дизајну API-ја пре имплементације, што доводи до слабе повезаности (loose coupling) и високе кохезије (high cohesion). Додатни ефекат — документација живим кодом: тестови служе као спецификација понашања модула, која је увек актуелна.

Када TDD није одговарајући?

TDD није увек оптималан. UI компоненте је тешко тестирати у изолацији — за њих су ефикаснији snapshot тестови или визуелно регресионо тестирање (Percy, Chromatic). Прототиповање и истраживање (spike solutions) не захтевају тестове. Legacy код без тестова тешко је покрити кроз TDD — овде су прво потребни characterization tests (тестови који фиксирају тренутно понашање пре рефакторисања). У овим случајевима TDD се не укида у потпуности, већ прилагођава — пишу се тестови за измењену функционалност, а не за цео legacy код.

Unit тестирање у мобилним апликацијама

Мобилни развој има специфику: пословна логика је често помешана са UI кодом (Activity, ViewController, ViewModel), што отежава unit тестирање. Најбоља пракса — танки прикази, дебели ViewModel-и: преместите сву логику из UI компоненти у одвојене класе (UseCase, Repository, ViewModel) које се лако тестирају без емулатора. За Android и iOS постоје изворни фрејмворци unit тестирања који раде на JVM/Native без покретања уређаја.

Unit тестови на Android-у (JUnit + Mockito/Robolectric)

Android unit тестови се извршавају на локалној JVM без емулатора, што обезбеђује брзину извршавања — типичан тест траје мање од 100ms. JUnit 5 је главни runner. За ViewModel тестове користите kotlinx-coroutines-test за тестирање корутина и Turbine за тестирање StateFlow. Robolectric омогућава тестирање Android-зависних компоненти (Context, Resources) без емулатора, учитавајући shadow класе. За Compose тестове користите Compose UI Test — али то су већ UI тестови, не unit.

Unit тестови на iOS-у (XCTest + Quick/Nimble)

iOS unit тестови се пишу у Swift-у са XCTest (уграђен у Xcode). Quick + Nimble — BDD фрејмворци за читљивије тестове (describe/context/it). За моковање користите Cuckoo (генерација мокова) или SwiftyMocky. Swift подржава протоколе и injection зависности, што олакшава замену зависности. Кључна ствар: iOS unit тестови се извршавају на macOS симулатору, а не на стварном уређају. Тестови који захтевају хардверске функције (камера, Bluetooth) су интеграциони тестови.

Unit тестови на Flutter-у (flutter_test + Mockito)

Flutter unit тестови користе пакет flutter_test и извршавају се на Dart VM без емулатора. За моковање — пакет mockito са генератором кода (build_runner). Widget тестови (у истом пакету) тестирају појединачне виџете, али захтевају рендеровање и раде спорије — користите их само за проверу UI логике. Чиста Dart логика (модели, репозиторијуми, блокови) тестира се као обични Dart тестови без импортовања flutter_test.

dart
// Пример unitтеста на Flutter-у са mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';

@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';

void main() {
    late MockApiClient mockApi;
    late UserRepository repository;

    setUp(() {
        mockApi = MockApiClient();
        repository = UserRepository(mockApi);
    });

    test('fetchUser returns user when API succeeds', () async {
        // Arrange
        final expectedUser = User(id: 1, name: 'Test');
        when(mockApi.getUser(1))
            .thenAnswer((_) async => expectedUser);

        // Act
        final result = await repository.fetchUser(1);

        // Assert
        expect(result, expectedUser);
        verify(mockApi.getUser(1)).called(1);
    });
}

Најбоље праксе и типичне грешке

Ефикасно unit тестирање захтева дисциплину. Главно правило: тестирајте понашање, а не имплементацију. Тест не треба да зна како је модул имплементиран изнутра (које приватне методе се позивају, којим редоследом). Ако је тест везан за имплементацију, ломи се при сваком рефакторисању и губи вредност. Тест проверава уговор: при улазу X треба да буде излаз Y. Изузетак — тестови за алгоритме са критичним перформансама, где је редослед позива важан.

  • Једна провера по тесту — један assert или једна група повезаних assert-ова за једну логичку проверу
  • Избегавајте дуплирање — користите @BeforeEach / setUp за заједничку иницијализацију, параметризоване тестове за различите улазне податке
  • Не тестирајте приватне методе — тестирајте кроз јавни API. Ако приватни метод није покривен, значи да његова логика није видљива споља
  • Покривајте граничне случајеве — празне колекције, null/undefined, негативне бројеве, максималне вредности
  • Не користите Thread.sleep у тестовима — то чини тестове спорим и нестабилним. Користите тест timeout-е и корутине

Који ниво покривености је довољан?

100% покривеност је недостижан и непотребан циљ. Према Google Testing Blog (2025), оптималан ниво покривености за unit тестове је 70-80% линија кода. 100% покривеност се често постиже тестирањем гетера, сетера и конструктора, што не доноси вредност. Фокусирајте се на критичну пословну логику: сложена израчунавања, валидацију, обраду грешака, ивичне случајеве. Користите JaCoCo (Java), Coverage.py (Python), Istanbul (JS) за мерење и подесите праг у CI — пад израде при покривености мањој од 60%.

CI/CD и unit тестови

Unit тестови су прва фаза сваког CI/CD пајплајна. Извршавају се при сваком push-у у репозиторијум, пре израде и деплоја. Просечан временски оквир извршавања unit тестова у пројекту не би требало да прелази 5 минута — ако је дужи, тестови престају да буду „брзи” и програмери престају да их покрећу локално. Поделите тестове на брзе (unit) и споре (интеграционе) и покрећите их у различитим фазама пајплајна. Користите паралелно извршавање и fail-fast за убрзање.

Често постављана питања

Чиме се unit тест разликује од интеграционог?

Unit тест проверава један модул у изолацији, замењујући спољне зависности моковима. Интеграциони тест проверава интеракцију између више стварних компоненти (база података, API, систем датотека). Unit тестови се извршавају у милисекундама, интеграциони у секундама. У пирамиди тестирања unit тестови заузимају 70%.

Који фрејмворк изабрати за unit тестове?

Избор зависи од платформе: JUnit 5 за Java/Kotlin, XCTest за iOS/Swift, pytest за Python, Jest/Vitest за JavaScript/TypeScript, flutter_test за Flutter. За моковање користите Mockito (Java), Cuckoo (iOS), unittest.mock (Python) или vitest.mock (JS). Сви савремени фрејмворци подржавају параметризоване тестове, уграђене assert-е и паралелно покретање.

Шта су F.I.R.S.T. принципи тестирања?

Fast — тест се извршава у милисекундама. Isolated — не зависи од других тестова и спољних система. Repeatable — даје исти резултат на свакој машини. Self-validating — проверава резултат аутоматски. Timely — написан пре или синхроно са кодом. Кршење макар једног принципа смањује ефикасност тестирања.

Да ли треба писати unit тестове за ViewModel у Android/iOS?

Да, обавезно. ViewModel садржи пословну логику — обраду догађаја, трансформацију података, управљање стањем. На Android-у користите kotlinx-coroutines-test за корутине и Turbine за тестирање StateFlow. На iOS-у тестирајте Combine Publishers или async/await у ViewModel. ViewModel тестови су чисти unit тестови који раде на JVM/macOS без емулатора.

Како тестирати код са мрежним захтевима?

Мрежни захтеви у unit тестовима се не извршавају — замењују се моковима HTTP клијента. На Android-у користите MockWebServer (OkHttp) — он подиже локални HTTP сервер, што је пожељније од мокова јер репродукује стварну мрежну интеракцију. MockWebServer пружа изолацију без губитка реалистичности. За iOS — OHHTTPStubs или URLProtocol за пресретање и замену одговора.

Резиме

  • Unit тестирање — провера појединачних модула у изолацији од спољних зависности са брзом повратном информацијом
  • AAA структура — Arrange (припрема), Act (акција), Assert (провера) — стандардни шаблон теста
  • Мокови и стабови — тест дублери за изолацију: мокови проверавају позиве, стабови враћају вредности
  • TDD — развој кроз тестирање (Red-Green-Refactor) смањује дефекте за 40-80%
  • FIRST принципи — Fast, Isolated, Repeatable, Self-validating, Timely — основа квалитетног теста
  • Платформни алати — JUnit 5 (Android), XCTest (iOS), flutter_test (Flutter), Jest (React Native)
  • Покривеност 70-80% — оптималан ниво за критичну пословну логику, гетери и сетери не захтевају тестове

Развићемо мобилну апликацију под кључ

IT Sectr креира iOS и Android апликације за стартапе и предузећа од 2017. године. Саветоваћемо вас и предложити најбоље решење.

Разговарајте о пројекту

Прочитајте такође