Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Unix & Linux ServerKDE Sets Ambitious Goals for 2026 and Beyond(23.09.2026 um 22:28 Uhr)
Unix & Linux ServerDSA-6510-1 xdg-dbus-proxy - security update(23.09.2026 um 02:00 Uhr)
Sichere ProgrammierungAPI & API Rest(23.09.2026 um 22:22 Uhr)
Unix & Linux ServerKDE Sets Ambitious Goals for 2026 and Beyond(23.09.2026 um 22:28 Uhr)
Unix & Linux ServerDSA-6510-1 xdg-dbus-proxy - security update(23.09.2026 um 02:00 Uhr)
Sichere ProgrammierungAPI & API Rest(23.09.2026 um 22:22 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Aplicando testes unitários com XCTest em um projeto iOS numa arquitetura VIP

Testes unitários são essenciais para garantir que nosso código funcione conforme o esperado. Eles vão além de validar funcionalidades durante o desenvolvimento - previnem regressões futuras, pois alterações no código podem impactar comporta…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Testes unitários são essenciais para garantir que nosso código funcione conforme o esperado. Eles vão além de validar funcionalidades durante o desenvolvimento - previnem regressões futuras, pois alterações no código podem impactar comportamentos existentes. Os testes ajudam a identificar esses problemas antes que cheguem ao usuário final.



Além disso, são uma forma de documentar o comportamento esperado do sistema, facilitar refatorações e reduzir tempo de debug.



Dando continuidade ao projeto desenvolvido com a arquitetura VIP, foram implementadas classes que conformam com protocolos específicos para cada componente (Interactor, Presenter e View). Essa abordagem traz duas vantagens principais:



Facilidade de Testes: A utilização de protocolos permite a criação de mocks ou spies, simplificando a escrita de testes unitários.



Desacoplamento: Cada componente pode ser testado isoladamente, garantindo maior modularidade e manutenibilidade.



Serão desenvolvidos testes unitários para os três principais componentes da arquitetura:




  • Interactor (Lógica de negócios)

  • Presenter (Transformação de dados para exibição)

  • View (Exibição e interação do usuário)






Uso de XCTest



O XCTest é a solução oficial da Apple, mas há outras opções no mercado como Quick/Nimble, Cuckoo e OHHTTPStubs.



Possibilidades de uso de XCTest e outras tecnologias




























Framework Vantagens Melhor Caso de Uso
XCTest Integração nativa, performance Projetos Apple puros
Quick/Nimble Sintaxe legível, BDD Times acostumados com Ruby/RSpec
Cuckoo Geração automática de mocks Projetos com muitas dependências


A fim de praticidade, simplicidade, e a integração ao Xcode sem dependências externas, usaremos o XCTest



Para adicioná-lo ao seu projeto, crie um novo Target a partir de File





E então selecione a opção Unit Testing Bundle





A estrutura de arquivos ficará assim, separando Mocks, Responses, e uma pasta para cada parte do VIP








Criando os Mocks






Tipos de Test Doubles





  • Spy: Registra chamadas para verificação posterior (como usado no artigo)


  • Mock: Possui expectativas pré-definidas que falham o teste se não forem atendidas


  • Stub: Retorna respostas pré-programadas sem verificação


  • Fake: Implementação simplificada para testes (ex: banco de dados em memória)



Nossos *Spy são na verdade combinações de Spy+Stub, pois:




  1. Registram chamadas (Spy)

  2. Permitem configurar respostas (Stub)



Como temos protocolos no nosso projeto, podemos criar objetos do tipo Spy para conformar com esses protocolos, e então, ao inicializar um Interactor, Presenter, ou View, passaremos nosso objeto mockado à ele.




Um Spy é um tipo de test double (objeto de teste) que:




  • Registra informações sobre como foi chamado (métodos, argumentos, frequência).

  • Permite verificação posterior no teste (ex: "esse método foi chamado 1 vez?").







Interactor



Usado para instanciar a view controller, rastreará a quantidade de chamadas a fetchArticles e didSelectArticle, e também os parâmetros recebidos em cada requisição




@testable import CareerAppUIKit

final class ArticlesInteractorSpy: ArticlesBusinessLogic {

// MARK: - Call Tracking
private(set) var fetchArticlesCallCount = 0
private(set) var didSelectArticleCallCount = 0
private(set) var lastFetchRequest: Articles.FetchArticles.Request?
private(set) var lastSelectRequest: Articles.DidSelectArticle.Request?

// MARK: - ArticlesBusinessLogic
func fetchArticles(request: Articles.FetchArticles.Request) {
fetchArticlesCallCount += 1
lastFetchRequest = request
}

func didSelectArticle(request: Articles.DidSelectArticle.Request) {
didSelectArticleCallCount += 1
lastSelectRequest = request
}

// MARK: - Test Helpers
func reset() {
fetchArticlesCallCount = 0
didSelectArticleCallCount = 0
lastFetchRequest = nil
lastSelectRequest = nil
}
}









Presenter



Usado para instanciar o Interactor, rastreará as chamadas de formatação (presentArticles, presentError, presentLoading) e os dados recebidos e enviados para a View.




@testable import CareerAppUIKit

final class ArticlesPresenterSpy: ArticlesPresentationLogic {

// MARK: - Call Tracking
private(set) var presentArticlesCallCount = 0
private(set) var presentErrorCallCount = 0
private(set) var presentLoadingCallCount = 0
private(set) var lastArticlesResponse: Articles.FetchArticles.Response?
private(set) var lastErrorResponse: Articles.PresentError.Response?
private(set) var lastLoadingResponse: Articles.PresentLoading.Response?

// MARK: - ArticlesPresentationLogic
func presentArticles(response: Articles.FetchArticles.Response) {
presentArticlesCallCount += 1
lastArticlesResponse = response
}

func presentError(response: Articles.PresentError.Response) {
presentErrorCallCount += 1
lastErrorResponse = response
}

func presentLoading(response: Articles.PresentLoading.Response) {
presentLoadingCallCount += 1
lastLoadingResponse = response
}

// MARK: - Test Helpers
func reset() {
presentArticlesCallCount = 0
presentErrorCallCount = 0
presentLoadingCallCount = 0
lastArticlesResponse = nil
lastErrorResponse = nil
lastLoadingResponse = nil
}
}









Router



Usado para instanciar o Interactor, rastreará as chamadas a routeToArticleDetail e os IDs de artigos passados durante a navegação.




@testable import CareerAppUIKit

final class ArticlesRouterSpy: ArticlesRoutingLogic {

// MARK: - Call Tracking
private(set) var routeToArticleDetailCallCount = 0
private(set) var routedArticleIds: [Int] = []

// MARK: - ArticlesRoutingLogic
func routeToArticleDetail(id: Int) {
routeToArticleDetailCallCount += 1
routedArticleIds.append(id)
}

// MARK: - Test Helpers
func reset() {
routeToArticleDetailCallCount = 0
routedArticleIds = []
}
}









View



Usada para instanciar o Presenter, rastreará as chamadas de atualização de UI (displayArticles, displayLoading, displayError) e os estados de loading e mensagens de erro.




@testable import CareerAppUIKit

final class ArticlesViewSpy: ArticlesDisplayLogic {

// MARK: - Call Tracking
private(set) var displayArticlesCallCount = 0
private(set) var displayLoadingCallCount = 0
private(set) var displayErrorCallCount = 0
private(set) var lastDisplayedArticles: [DisplayedArticle]?
private(set) var lastLoadingState: Bool?
private(set) var lastErrorMessage: String?

// MARK: - ArticlesDisplayLogic
func displayArticles(viewModel: Articles.FetchArticles.ViewModel) {
displayArticlesCallCount += 1
lastDisplayedArticles = viewModel.displayedArticles
}

func displayLoading(viewModel: Articles.PresentLoading.Response) {
displayLoadingCallCount += 1
lastLoadingState = viewModel.isLoading
}

func displayError(viewModel: Articles.PresentError.Response) {
displayErrorCallCount += 1
lastErrorMessage = viewModel.errorMessage
}

func displayArticleDetail(_ articleDetail: ArticleDetail) {
// Implementação se necessário
}

// MARK: - Test Helpers
func reset() {
displayArticlesCallCount = 0
displayLoadingCallCount = 0
displayErrorCallCount = 0
lastDisplayedArticles = nil
lastLoadingState = nil
lastErrorMessage = nil
}
}









Worker



Usado para instanciar o Interactor, permite simular respostas de sucesso/erro da API e define dados fixos para testes.




@testable import CareerAppUIKit

final class ArticlesWorkerSpy: ArticlesWorkerProtocol {

// MARK: - Properties
private var stubbedArticles: [Article] = []
private var stubbedError: Error?

// MARK: - Public Methods
func fetchArticles(completion: @escaping (Result<[Article], Error>) -> Void) {
if let error = stubbedError {
completion(.failure(error))
} else {
completion(.success(stubbedArticles))
}
}

// MARK: - Stub Methods
func stubSuccessResponse(with articles: [Article]) {
stubbedArticles = articles
stubbedError = nil
}

func stubFailureResponse(with error: Error) {
stubbedError = error
stubbedArticles = []
}

func reset() {
stubbedArticles = []
stubbedError = nil
}
}









Estrutura dos testes





  • Setup e Teardown:





    • setUp(): Inicializa os componentes necessários antes de cada teste


    • tearDown(): Limpa o estado após cada teste








Importantes para garantir isolamento entre testes e evita vazamento de estado




  • Organização com MARK:



Facilita navegação e manutenção do código



Separação clara em seções (Properties, Lifecycle, Test Cases, Helpers)




  • Padrão de Nomenclatura:



Nomearemos "contando uma história" do que o teste faz, para deixá-lo melhor documentado e autoexplicativo.



test_Condição_Ação_ResultadoEsperado

(ex: test_givenSuccessfulResponse_whenFetchingArticles_shouldPresentArticles)





  • Estrutura Given-When-Then:




    • Given: Prepara o estado inicial

    • When: Executa a ação sendo testada

    • Then: Verifica os resultados





Para clareza em cada fluxo do teste, usaremos esse padrão de construção.




  • XCTUnwrap para tratamento de opcionais



Solução para um tratamento seguro de desembrulhar opcionais, além de falhar imediatamente o teste se os valores essenciais forem nulos, sua integração mostra a linha exata da falha, com uma mensagem clara e elimina aninhamento de guard/else




func test_userProfile_shouldDisplayCorrectName() throws {
// Arrange
let user: User? = User(name: "Ana", age: 28)

// Act & Assert
let unwrappedUser = try XCTUnwrap(user, "O objeto user não deveria ser nil")
XCTAssertEqual(unwrappedUser.name, "Ana", "O nome deveria ser Ana")
}









Escrevendo os testes do Interactor



Os testes do Interactor serão para validar as regras de negócio, validar os casos de sucesso e erro vindos do Worker e se os encaminhamentos idos para o Presenter estão adequados.




import XCTest
@testable import CareerAppUIKit

final class ArticlesInteractorTests: XCTestCase {

// MARK: - Properties
private var interactor: ArticlesInteractor?
private var presenter: ArticlesPresenterSpy?
private var worker: ArticlesWorkerSpy?
private var router: ArticlesRouterSpy?

// MARK: - Test Lifecycle
override func setUp() {
super.setUp()
setupTestComponents()
}

override func tearDown() {
cleanUpTestComponents()
super.tearDown()
}

// MARK: - Test Cases

func test_fetchArticles_whenWorkerSucceeds_shouldCallPresentArticles() throws {
// Given
let worker = try XCTUnwrap(worker, "Worker should not be nil")
let presenter = try XCTUnwrap(presenter, "Presenter should not be nil")
let interactor = try XCTUnwrap(interactor, "Interactor should not be nil")

worker.stubSuccessResponse(with: [])

// When
interactor.fetchArticles(request: Articles.FetchArticles.Request())

// Then
XCTAssertEqual(presenter.presentArticlesCallCount, 1, "Should call present articles exactly once")
XCTAssertEqual(presenter.presentErrorCallCount, 0, "Should not call present error")
XCTAssertNotNil(presenter.lastArticlesResponse, "Should receive articles response")
}

func test_fetchArticles_whenWorkerFails_shouldCallPresentError() throws {
// Given
let worker = try XCTUnwrap(worker, "Worker should not be nil")
let presenter = try XCTUnwrap(presenter, "Presenter should not be nil")
let interactor = try XCTUnwrap(interactor, "Interactor should not be nil")

let expectedError = NetworkError.decodingError
worker.stubFailureResponse(with: expectedError)

// When
interactor.fetchArticles(request: Articles.FetchArticles.Request())

// Then
XCTAssertEqual(presenter.presentErrorCallCount, 1, "Should call present error exactly once")
XCTAssertEqual(presenter.presentArticlesCallCount, 0, "Should not call present articles")
XCTAssertEqual(presenter.lastErrorResponse?.errorMessage, expectedError.localizedDescription)
}

func test_didSelectArticle_shouldRouteWithCorrectID() throws {
// Given
let router = try XCTUnwrap(router, "Router should not be nil")
let interactor = try XCTUnwrap(interactor, "Interactor should not be nil")

let testId = 123
let request = Articles.DidSelectArticle.Request(id: testId)

// When
interactor.didSelectArticle(request: request)

// Then
XCTAssertEqual(router.routeToArticleDetailCallCount, 1, "Should call route to detail exactly once")
XCTAssertEqual(router.routedArticleIds.first, testId, "Should route with correct article ID")
}

// MARK: - Helper Methods
private func setupTestComponents() {
let newPresenter = ArticlesPresenterSpy()
let newWorker = ArticlesWorkerSpy()
let newRouter = ArticlesRouterSpy()

presenter = newPresenter
worker = newWorker
router = newRouter

interactor = ArticlesInteractor(
presenter: newPresenter,
worker: newWorker,
router: newRouter
)
}

private func cleanUpTestComponents() {
interactor = nil
presenter?.reset()
presenter = nil
worker?.reset()
worker = nil
router?.reset()
router = nil
}
}









Escrevendo os testes do Presenter



Os testes do Presenter serão para validar os estados de loading/error e a formatação correta dos modelos, vindos do Interactor.




import XCTest
@testable import CareerAppUIKit

final class ArticlesPresenterTests: XCTestCase {

// MARK: - Properties
private var sut: ArticlesPresenter?
private var viewSpy: ArticlesViewSpy?

// MARK: - Test Lifecycle
override func setUp() {
super.setUp()
sut = ArticlesPresenter()
viewSpy = ArticlesViewSpy()
sut?.viewController = viewSpy
}

override func tearDown() {
sut = nil
viewSpy?.reset()
viewSpy = nil
super.tearDown()
}

// MARK: - Test Cases

func test_presentArticles_shouldFormatAndDisplayData() throws {
// Given
let articles = try loadArticlesFromJSON()
let response = Articles.FetchArticles.Response(articles: articles)
let presenter = try XCTUnwrap(sut, "Presenter não deveria ser nil")
let view = try XCTUnwrap(viewSpy, "ViewSpy não deveria ser nil")

// When
presenter.presentArticles(response: response)

// Then
XCTAssertEqual(view.displayArticlesCallCount, 1, "Deveria chamar displayArticles uma vez")
XCTAssertEqual(view.lastDisplayedArticles?.count, articles.count, "Deveria exibir todos os artigos")
XCTAssertEqual(view.lastDisplayedArticles?.first?.title, articles.first?.title, "Títulos deveriam corresponder")
}

func test_presentLoading_shouldUpdateViewState() throws {
// Given
let isLoading = true
let presenter = try XCTUnwrap(sut, "Presenter não deveria ser nil")
let view = try XCTUnwrap(viewSpy, "ViewSpy não deveria ser nil")

// When
presenter.presentLoading(response: Articles.PresentLoading.Response(isLoading: isLoading))

// Then
XCTAssertEqual(view.displayLoadingCallCount, 1, "Deveria chamar displayLoading uma vez")
XCTAssertEqual(view.lastLoadingState, isLoading, "Estado de loading deveria ser true")
}

func test_presentError_shouldDisplayErrorMessage() throws {
// Given
let testError = NetworkError.noData
let presenter = try XCTUnwrap(sut, "Presenter não deveria ser nil")
let view = try XCTUnwrap(viewSpy, "ViewSpy não deveria ser nil")

// When
presenter.presentError(response: Articles.PresentError.Response(errorMessage: testError.localizedDescription))

// Then
XCTAssertEqual(view.displayErrorCallCount, 1, "Deveria chamar displayError uma vez")
XCTAssertEqual(view.lastErrorMessage, testError.localizedDescription, "Mensagem de erro deveria corresponder")
}

// MARK: - Helper Methods

private func loadArticlesFromJSON() throws -> [Article] {
let bundle = Bundle(for: type(of: self))
guard let url = bundle.url(forResource: "articles", withExtension: "json") else {
throw NSError(domain: "Tests", code: 1, userInfo: [NSLocalizedDescriptionKey: "Arquivo JSON não encontrado"])
}

let data = try Data(contentsOf: url)
let decoder = JSONDecoder()
return try decoder.decode([Article].self, from: data)
}
}









Escrevendo os testes da View



Os testes da View serão para validar os comportamentos da UI, as interações do usuário e sua comunicação com o Interactor.




import XCTest
@testable import CareerAppUIKit

class ArticlesDataStoreSpy: ArticlesDataStoreProtocol {
var displayedArticles: [DisplayedArticle] = []
}

final class ArticlesViewControllerTests: XCTestCase {

// MARK: - Test Components
private var sut: ArticlesViewController?
private var interactorSpy: ArticlesInteractorSpy?
private var dataStoreSpy: ArticlesDataStoreSpy?
private let testCollectionView = UICollectionView(
frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout()
)

// MARK: - Test Lifecycle
override func setUp() {
super.setUp()
do {
try setupTestComponents()
} catch {
XCTFail("Falha no setup: \(error.localizedDescription)")
}
}

override func tearDown() {
cleanUpTestComponents()
super.tearDown()
}

// MARK: - Test Cases

func test_viewDidLoad_shouldFetchArticles() throws {
// When
_ = try XCTUnwrap(sut).view

// Then
XCTAssertEqual(interactorSpy?.fetchArticlesCallCount, 1)
}

func test_didSelectArticle_shouldNotifyInteractor() throws {
// Given
let testArticle = DisplayedArticle(
id: 1, title: "Test", description: "Desc",
publishDate: "Jan 1", imageUrl: nil,
authorName: "Author", tags: "tag1,tag2"
)
dataStoreSpy?.displayedArticles = [testArticle]
let indexPath = IndexPath(item: 0, section: 0)

// When
try XCTUnwrap(sut).collectionView(
testCollectionView,
didSelectItemAt: indexPath
)

// Then
XCTAssertEqual(interactorSpy?.didSelectArticleCallCount, 1)
XCTAssertEqual(try XCTUnwrap(interactorSpy?.lastSelectRequest?.id), 1)
}

func test_emptyState_shouldShowEmptyCell() throws {
// Given
dataStoreSpy?.displayedArticles = []
let indexPath = IndexPath(item: 0, section: 0)

// When
let cell = try XCTUnwrap(sut).collectionView(
testCollectionView,
cellForItemAt: indexPath
)

// Then
XCTAssertTrue(cell is EmptyArticlesCell)
}

func test_collectionView_shouldReturnCorrectNumberOfItems() throws {
// Given
let testArticles = [
DisplayedArticle(id: 1, title: "Test 1", description: "Desc 1",
publishDate: "Jan 1", imageUrl: nil,
authorName: "Author 1", tags: "tag1"),
DisplayedArticle(id: 2, title: "Test 2", description: "Desc 2",
publishDate: "Jan 2", imageUrl: nil,
authorName: "Author 2", tags: "tag2")
]
dataStoreSpy?.displayedArticles = testArticles

// When
let numberOfItems = try XCTUnwrap(sut).collectionView(
testCollectionView,
numberOfItemsInSection: 0
)

// Then
XCTAssertEqual(numberOfItems, testArticles.count)
}

// MARK: - Test Setup
private func setupTestComponents() throws {
interactorSpy = ArticlesInteractorSpy()
dataStoreSpy = ArticlesDataStoreSpy()
sut = ArticlesViewController(interactor: try XCTUnwrap(interactorSpy))
sut?.dataStore = try XCTUnwrap(dataStoreSpy)
sut?.loadViewIfNeeded()
}

private func cleanUpTestComponents() {
sut = nil
interactorSpy?.reset()
interactorSpy = nil
dataStoreSpy = nil
}
}






A implementação de testes unitários em arquitetura VIP mostra toda sua potência quando:




  • Cada componente tem responsabilidades bem definidas

  • As dependências são invertidas via protocolos

  • Os testes focam em comportamentos, não implementações

CTI Threat Relationship Graph3 Knoten / 2 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
IR-PLAYBOOK-RCE
HIGH
SOC Incident Playbook: Remote Code Execution (RCE) Defense
1-Click Detection Engineering: Sigma & YARA Rules
SOC Ready
title: Detect Exploitation - Aplicando testes unitários com XCTest em um projeto iOS numa arquitetura VIP
id: 99478dcc-1026-4bb1-9550-acdb96aab4c8
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-23
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-23"
        description = "YARA Signature for "
    strings:
        $str = "Aplicando testes unitários com" ascii wide
    condition:
        any of them
}
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Aplicando testes unitários com XCTest em um projeto iOS numa arquitetura VIP

Thematisch verwandte Begriffe: Aplicando, testes, unitários, XCTest · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-90904 | Joomla Extension - joomshaper.com - Broken Access Control (ACL Bypass) i…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick