Pisanje unit testova korištenjem ChatGPT-a kao vlastitog J.A.R.V.I.S.-a

Autor: Željka Strmečki, Undabot, iOS Developer

Važnost pisanja unit testova opće je poznata među programerima. Međutim, dodavanje testova projektu koji je već dovršen može biti poprilično zahtjevan zadatak. Srećom, ne moramo to raditi sami – možemo iskoristiti ChatGPT, našu vlastitu verziju Tony Starkovog AI pomoćnika J.A.R.V.I.S.-a, da nam pomogne.

Otkako je predstavljen, ChatGPT je postao vrlo traženi alat za različite zadatke, uključujući razvoj softvera. Zahvaljujući naprednom jezičnom modelu kojeg je razvio Open AI, ima sposobnost generiranja tekstova sličnih ljudskom jeziku. Treniran je na ogromnom skupu podataka, što mu omogućuje duboko razumijevanje jezika i davanje relevantnih odgovora.  Može se prilagoditi specifičnim slučajevima uporabe, poput generiranja dijelova koda, odgovaranja na tehnička pitanja ili davanja preporuka na temelju ulaznih podataka korisnika. Zbog naprednih mogućnosti, nije neuobičajeno da programeri traže pomoć ChatGTP-a za zadatke kao što je pisanje unit testova u svojim projektima.

Za kakav projekt pišemo testove

U ovom članku pokazat ćemo vam kako nam je ChatGPT pomogao u generiranju unit testova za postojeći projekt napisan u programskom jeziku Swift. Fokus je bio na testiranju ViewModel klasa koje uključuju gatewaye i ograničenu logiku.

Klase korištene u ovom primjeru malo su modificirane da bi poslužile kao dobar primjer koda prije testiranja, te su skraćene da budu jasnije.

Dobra priprema je ključna

Prije nego se započne s pisanjem unit testova, važno je dobro razumjeti postojeći projekt i klase koje treba testirati. Analizom klasa i ubacivanjem svakog ovisnog objekta u konstruktor, osigurali smo da ChatGPT generira jasnije i opsežnije testove.

Uz to smo uključili Quick i Nimble – popularne frameworke za pisanje testova u Swiftu. Quick je behavior-driven development framework koji pruža jednostavnu, čitljivu sintaksu za pisanje testova. Nimble je framework koji olakšava pisanje tvrdnji (assertions) u testovima. U usporedbi s Xcode assertionsima i testovima, oni nude intuitivniju i ekspresivniju sintaksu, što olakšava pisanje jasnih i sažetih testova.

Kako početi

Prvo smo kopirali klasu prikazanu u nastavku i dali ChatGPT-u jednostavnu naredbu: “Please write tests using Quick and Nimble for the following Swift code”.  Kad se od ChatGPT-a traži generiranje koda, važno mu je dati što je više informacija moguće. Da smo tražili samo “Write a test for this class”, dobili bismo značajno drugačiji rezultat.

import Foundation
import RxSwift
protocol SettingsViewModelDelegate: AnyObject {
func settingsDidChange()
func showProgressIndicator()
func hideProgressIndicator()
}
protocol SettingsCoordinatorDelegate: AnyObject {
func showErrorScreen()
}
class SettingsViewModel {
weak var delegate: SettingsViewModelDelegate?
weak var coordinatorDelegate: SettingsCoordinatorDelegate?
let userInfo: UserInfo
var notificationsEnabled: Bool
var privacyEnabled: Bool
let userDefaultsManager: UserDefaultsManager
let gateway: SettingsGateway
let disposeBag = DisposeBag()
init(userInfo: UserInfo, userDefaultsManager: UserDefaultsManager = UserDefaultsManager(), gateway: SettingsGateway = SettingsGateway()) {
self.userInfo = userInfo
self.userDefaultsManager = userDefaultsManager
self.notificationsEnabled = userDefaultsManager.notificationsEnabled
self.privacyEnabled = userDefaultsManager.privacyEnabled
self.gateway = gateway
}
func changeSettings(notificationsEnabled: Bool, privacyEnabled: Bool) {
guard let userId = userDefaultsManager.userId else {
return
}
delegate?.showProgressIndicator()
let body = SettingsBody(userId: userId, notificationsEnabled: notificationsEnabled, privacyEnabled: privacyEnabled)
gateway
.changeSettings(body: body)
.subscribeOnBackgroundObserveOnMain()
.subscribe(onSuccess: { [weak self] response in
guard let self else { return }
self.notificationsEnabled = response.notificationsEnabled
self.privacyEnabled = response.privacyEnabled
self.userDefaultsManager.notificationsEnabled = response.notificationsEnabled
self.userDefaultsManager.privacyEnabled = response.privacyEnabled
self.delegate?.settingsDidChange()
}, onFailure: { [weak self] _ in
guard let self else { return }
self.delegate?.hideProgressIndicator()
self.coordinatorDelegate?.showErrorScreen()
}).disposed(by: disposeBag)
}
}

ChatGPT je generirao dobro strukturirani test koji zadovoljava sve potrebne specifikacije:

  1. Test je potklasa klase QuickSpec:
    • Ima metodu spec() koja uključuje klasu SettingsViewModel koja se testira i sve potrebne simulirane ovisnosti (mockove)
  1. ViewModel i mockovi su inicijalizirani u beforeEach bloku
  2. describe blokovi nazvani su prema metodama koje se testiraju
  3. context blokovi dijele različite slučajeve izvršenja i ponašanja za metodu
  4. it blok sadrži test u kojem se poziva metoda na viewModelu i provjerava koristeći expect klauzulu
import Quick
import Nimble
import RxSwift
@testable import Test_Project
class SettingsViewModelSpec: QuickSpec {
override func spec() {
var viewModel: SettingsViewModel!
var delegate: SettingsViewModelDelegateMock!
var coordinatorDelegate: SettingsCoordinatorDelegateMock!
var userDefaultsManager: UserDefaultsManagerMock!
var gateway: SettingsGatewayMock!
beforeEach {
delegate = SettingsViewModelDelegateMock()
coordinatorDelegate = SettingsCoordinatorDelegateMock()
userDefaultsManager = UserDefaultsManagerMock()
gateway = SettingsGatewayMock()
viewModel = SettingsViewModel(
userInfo: UserInfo(),
userDefaultsManager: userDefaultsManager,
gateway: gateway
)
viewModel.delegate = delegate
viewModel.coordinatorDelegate = coordinatorDelegate
}
describe("changeSettings") {
context("when the user ID exists") {
beforeEach {
userDefaultsManager.userId = "123"
}
context("when the gateway call is successful") {
beforeEach {
gateway.changeSettingsResult = .success(SettingsResponse(
notificationsEnabled: true,
privacyEnabled: false
))
viewModel.changeSettings(notificationsEnabled: true, privacyEnabled: false)
}
it("calls showProgressIndicator on delegate") {
expect(delegate.showProgressIndicatorCalled).to(beTrue())
}
it("updates notificationsEnabled and privacyEnabled on the view model") {
expect(viewModel.notificationsEnabled).to(beTrue())
expect(viewModel.privacyEnabled).to(beFalse())
}
it("updates notificationsEnabled and privacyEnabled on the userDefaultsManager") {
expect(userDefaultsManager.notificationsEnabled).to(beTrue())
expect(userDefaultsManager.privacyEnabled).to(beFalse())
}
it("calls settingsDidChange on delegate") {
expect(delegate.settingsDidChangeCalled).to(beTrue())
}
it("doesn't call hideProgressIndicator on delegate") {
expect(delegate.hideProgressIndicatorCalled).to(beFalse())
}
it("doesn't call showErrorScreen on coordinatorDelegate") {
expect(coordinatorDelegate.showErrorScreenCalled).to(beFalse())
}
}
context("when the gateway call fails") {
beforeEach {
gateway.changeSettingsResult = .failure(APIError.unknown)
viewModel.changeSettings(notificationsEnabled: true, privacyEnabled: false)
}
it("calls showProgressIndicator on delegate") {
expect(delegate.showProgressIndicatorCalled).to(beTrue())
}
it("doesn't update notificationsEnabled and privacyEnabled on the view model") {
expect(viewModel.notificationsEnabled).to(beFalse())
expect(viewModel.privacyEnabled).to(beFalse())
}
it("

Kao što možete vidjeti, proces ispisivanja je bio prekinut. Međutim, naredbom “Continue previous response” , ChatGPT obično može nastaviti gdje je stao. U nekim slučajevima može krenuti ispočetka, što je jedan od izazova korištenja ChatGPT-a za pisanje testova. No, pomoću naredbe “Please continue the following test” i davanjem prekinutog testa uspio je dovršiti generiranje preostalog koda, što je dalo sljedeći rezultat:

it("doesn't update notificationsEnabled and privacyEnabled on the view model") {
expect(viewModel.notificationsEnabled).to(beFalse())
expect(viewModel.privacyEnabled).to(beFalse())
}
it("doesn't update notificationsEnabled and privacyEnabled on the userDefaultsManager") {
expect(userDefaultsManager.notificationsEnabled).to(beFalse())
expect(userDefaultsManager.privacyEnabled).to(beFalse())
}
it("doesn't call settingsDidChange on delegate") {
expect(delegate.settingsDidChangeCalled).to(beFalse())
}
it("doesn't call hideProgressIndicator on delegate") {
expect(delegate.hideProgressIndicatorCalled).to(beFalse())
}
it("doesn't call showErrorScreen on coordinatorDelegate") {
expect(coordinatorDelegate.showErrorScreenCalled).to(beFalse())
}
}
}
}

ChatGPT je učinkovito generirao popriličnu količinu koda, što nam je uštedjelo vrijeme i trud. Međutim, bilo je potrebno prilagoditi kod kako bi se osigurala funkcionalnost. Prilikom pokretanja testa u Xcodeu naišli smo na nekoliko grešaka. Nismo imali implementacije za SettingsViewModelDelegateMock, SettingsCoordinatorDelegateMock, UserDefaultsManagerMock i SettingsGatewayMock.

Da bismo riješili te probleme, zatražili smo od ChatGPT-a da generira mock za klasu UserDefaultsManager. Ovo je rezultat:

class UserDefaultsManagerMock: UserDefaultsManager {
var mockUserId: String?
var mockNotificationsEnabled: Bool = false
var mockPrivacyEnabled: Bool = false
override var userId: String? {
get {
return mockUserId
}
set {
mockUserId = newValue
}
}
override var notificationsEnabled: Bool {
get {
return mockNotificationsEnabled
}
set {
mockNotificationsEnabled = newValue
}
}
override var privacyEnabled: Bool {
get {
return mockPrivacyEnabled
}
set {
mockPrivacyEnabled = newValue
}
}
}

Takoder smo od ChatGPT-a zatražili da generira mockove za delegate klase tako što smo mu dali sljedeću naredbu: “Please continue previous test and write mocks for delegate and coordinatorDelegate”:

class SettingsViewModelDelegateMock: SettingsViewModelDelegate {
var showProgressIndicatorCalled = false
var settingsDidChangeCalled = false
var hideProgressIndicatorCalled = false
func showProgressIndicator() {
showProgressIndicatorCalled = true
}
func settingsDidChange() {
settingsDidChangeCalled = true
}
func hideProgressIndicator() {
hideProgressIndicatorCalled = true
}
}
class SettingsCoordinatorDelegateMock: SettingsCoordinatorDelegate {
var showErrorScreenCalled = false
func showErrorScreen() {
showErrorScreenCalled = true
}
}

Naposljetku, da dovršimo proces testiranja, zatražili smo od ChatGPT-a da napiše mockove za klasu SettingsGateway. Morali smo mu samo dati klasu i naredbu “Please write mocks for the SettingsGateway class”:

class SettingsGateway {
private let apiClient: APIClientProtocol
init(apiClient: APIClientProtocol = APIClient.shared) {
self.apiClient = apiClient
}
func changeSettings(body: SettingsBody) -> Single<ChangeSettingsResponse> {
let request = ChangeSettingsRequest(body: body)
return apiClient.perform(request: request)
}
}

ChatGPT je generirao sljedeće:

class SettingsGatewayMock: SettingsGatewayProtocol {
var changeSettingsResult: Single<ChangeSettingsResponse>!
func changeSettings(body: SettingsBody) -> Single<ChangeSettingsResponse> {
return changeSettingsResult
}
}

Nismo bili u potpunosti zadovoljni prvotnim rezultatom jer su nedostajale vrijednosti propertya u mocku, a inicijalna klasa zapravo nije odgovarala protokolu SettingsGatewayProtocol. Da to ispravimo, tražili smo ChatGPT da ispravi prethodni mock naredbom: “Please correct the previous mock “ i ažurirani rezultat je izgledao ovako:

class SettingsGatewayMock: SettingsGateway {
var changeSettingsResult: Single<ChangeSettingsResponse> = Single.just(ChangeSettingsResponse(success: true))
override func changeSettings(body: SettingsBody) -> Single<ChangeSettingsResponse> {
return changeSettingsResult
}
}

Nakon analize rezultata primijetili smo da metoda changeSettings(body: SettingsBody) vraća samo uspješan rezultat,, ali ne i grešku. Da to riješimo, dodali smo dodatni property errorToReturn.

Uz to smo zatražili od ChatGPT-a da generira objekt s određenim vrijednostima s naredbom “Please create an object for the following struct”:

struct ChangeSettingsResponse: Codable {
let status: Int
let notificationsEnabled: Bool
let privacyEnabled: Bool
}

Vratio je changeSettingsResponse objekt koji je korišten kao povratna vrijednost u metodi changeSettings:

gateway.changeSettingsResult = .failure(APIError.unknown)

ChatGPT nam u ovakvim slučajevima može smanjiti količinu vremena i truda potrošenog na ručno stvaranje objekta, posebice u situacijama kad se treba instancirati objekt s puno propertya.

Kao konačni rezultat, dobili smo verziju SettingsGatewayMock-a koja nam je bila potrebna:

class SettingsGatewayMock: SettingsGateway {
var errorToRetun: Error?
var changeSettingsResult: Single<ChangeSettingsResponse> = Single.just(ChangeSettingsResponse(status: 200, notificationsEnabled: true, privacyEnabled: false))
override func changeSettings(body: SettingsBody) -> Single<ChangeSettingsResponse> {
if let errorToReturn {
return Single.error(errorToReturn)
}
return changeSettingsResult
}
}

Nakon nekoliko izmjena uspjeli smo riješiti preostale greške pri pokretanju testova. Da bi poziv gatewaya bio uspješan, zamijenili smo

gateway.changeSettingsResult = .success(SettingsResponse(
notificationsEnabled: true,
privacyEnabled: false
))

s

gateway.changeSettingsResult = .failure(APIError.unknown)

Da bi poziv gatewaya vračao grešku, dodali smo:

gateway.changeSettingsResult = .failure(APIError.unknown)

umjesto

gateway.changeSettingsResult = .failure(APIError.unknown)

Također smo dodali ispravni inicijalizator za UserInfo klasu i s tim izmjenama uspješno smo dovršili testove.

Međutim, naši testovi nisu prolazili prilikom izvršavanja. Da bismo riješili taj problem, morali smo zamijeniti ‘to’ s ‘toEventually’ u expect klauzulama. Oboje, ‘to’ i ‘toEventually’ su matcheri (uspoređivači) koji se koriste da bi se potvrdila jednakost dvije vrijednosti. Razlika između njih je u vremenu njihove evaluacije.’To’ provjerava odgovara li stvarna vrijednost očekivanoj vrijednosti odmah, dok ‘toEventually’ kontinuirano provjerava stvarnu vrijednost dok se ne podudara s očekivanom vrijednosti ili dok vrijeme ne istekne. Druga opcija je posebice korisna prilikom testiranja asinkronog koda i zato smo odabrali koristiti ‘toEventually’ u našem slučaju.

Nakon posljednjih izmjena, naši testovi su uspješno prošli. S dobro postavljenim temeljima jednostavno je dodavati nove testove, bilo ručno ili uz pomoć ChatGPT-a, kako bismo pokrili sve moguće scenarije.

Koliko je učinkovita pomoć ChatGPT-a?

Dok se prethodni primjer čini jednostavnim, korištenje ChatGPT-a na stvarnom projektu zahtijevalo je malo više truda kako bi se postigao željeni rezultat. Međutim, uz jasnu komunikaciju i temeljito razumijevanje projekta i onoga što se treba testirati, ChatGPT se pokazao kao sjajan pomoćnik. Nakon dobivanja jasnih uputa, brzo je generirao značajnu količinu koda i uštedio na vremenu u repetitivnim zadacima kao što je pripremanje mockova.

Zaključak

Željka Strmečki, iOS Developer u Undabotu

ChatGPT je koristan alat za programere koji žele pojednostaviti i ubrzati proces testiranja. Unatoč povremenim problemima, kao što su greške ili nedostupnost, pokazao se kao odličan alat za uštedu vremena, posebno za projekte koji nisu razvijeni koristeći test-driven development pristup. Osim toga, izvrstan je resurs za programere svih razina znanja, uključujući one koji se tek počinju baviti testiranjem. Oni posebno mogu vježbati i s vremenom naučiti pisati učinkovite testove.

Da biste postali Ironman unit testiranja, ChatGPT vam može poslužiti kao osobni J.A.R.V.I.S. za generiranje temelja testova i stvaranje mockova i, uz neke preinake, možete osigurati da su testovi učinkoviti i točni.