diff --git a/AIProject/iCoTests/ImageProcessTestHelpers.swift b/AIProject/iCoTests/ImageProcessTestHelpers.swift index c198b10a..eedc5f14 100644 --- a/AIProject/iCoTests/ImageProcessTestHelpers.swift +++ b/AIProject/iCoTests/ImageProcessTestHelpers.swift @@ -9,26 +9,45 @@ import UIKit @testable import iCo // MARK: - Helper Methods -final class ImageProcessTestHelpers { - static func createTestImage(with text: String) -> UIImage { - let size = CGSize(width: 300, height: 100) - UIGraphicsBeginImageContextWithOptions(size, false, 0) +final class ImageProcessTestHelpers { + static func createTestImage(with text: String) -> CGImage? { + let width = 300 + let height = 100 + let colorSpace = CGColorSpaceCreateDeviceRGB() - let context = UIGraphicsGetCurrentContext() - context?.setFillColor(UIColor.white.cgColor) - context?.fill(CGRect(origin: .zero, size: size)) + // RGBA 8비트 구성으로 CGContext 생성 + guard let context = CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) else { + return nil + } + // 배경 흰색 + context.setFillColor(UIColor.white.cgColor) + context.fill(CGRect(x: 0, y: 0, width: width, height: height)) + + // UIKit 없이 텍스트를 직접 Core Graphics로 그리려면 NSAttributedString을 활용해야 함 + let textRect = CGRect(x: 10, y: 40, width: 280, height: 50) let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 20), .foregroundColor: UIColor.black ] - text.draw(at: CGPoint(x: 10, y: 40), withAttributes: attributes) - - let image = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() + let attributedText = NSAttributedString(string: text, attributes: attributes) + let framesetter = CTFramesetterCreateWithAttributedString(attributedText) + let path = CGMutablePath() + path.addRect(textRect) + let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedText.length), path, nil) + CTFrameDraw(frame, context) - return image ?? UIImage() + // CGContext → CGImage 생성 + return context.makeImage() } static func createMockCoinDTO() -> CoinDTO { diff --git a/AIProject/iCoTests/ImageProcessViewModelTests.swift b/AIProject/iCoTests/ImageProcessViewModelTests.swift index 4b41beeb..d0d6f7ff 100644 --- a/AIProject/iCoTests/ImageProcessViewModelTests.swift +++ b/AIProject/iCoTests/ImageProcessViewModelTests.swift @@ -33,7 +33,7 @@ final class ImageProcessViewModelTests: XCTestCase { // 이미지에서 글자를 찾지 못했을 때 알맞은 에러를 반환하는지? func testImageProcessViewModel_whenNoTextIsReturned_terminateWithError() async throws { // Given - mockImage = ImageProcessTestHelpers.createTestImage(with: "").cgImage + mockImage = ImageProcessTestHelpers.createTestImage(with: "") // When sut = ImageProcessViewModel() @@ -47,42 +47,11 @@ final class ImageProcessViewModelTests: XCTestCase { // Then XCTAssertFalse(sut.isLoading) XCTAssertFalse(sut.showAnalysisResultAlert) - - XCTAssertTrue(sut.showErrorMessage) - XCTAssertNotNil(sut.errorMessage) - XCTAssertTrue(sut.verifiedCoinList.isEmpty) - XCTAssertEqual(sut.errorMessage, ImageProcessError.noRecognizedText.description) - } - - // Alan에게서 코인 이름을 받아오지 못했을 때 알맞은 에러를 반환하는지? - func testImageProcessViewModel_whenNoConvertedSymbol_terminateWithError() async throws { - // Alan API키를 검증하고, 키가 없을 시에는 테스트 건너뛰기 - guard let apiKey = Bundle.main.infoDictionary?["ALAN_API_KEY_COIN_ID_EXTRACTION"] as? String, - !apiKey.isEmpty else { - throw XCTSkip("테스트에 필요한 Alan API 키를 읽어오지 못함") - } - - // Given - mockImage = ImageProcessTestHelpers.createTestImage(with: "bittcoin batcoin").cgImage - - // When - sut = ImageProcessViewModel() - sut.coinList = mockCoinList - sut.processImage(from: mockImage) - - if let task = sut.processImageTask { - _ = try? await task.value - } - - // Then - XCTAssertFalse(sut.isLoading) - XCTAssertFalse(sut.showAnalysisResultAlert) - XCTAssertTrue(sut.showErrorMessage) XCTAssertNotNil(sut.errorMessage) XCTAssertTrue(sut.verifiedCoinList.isEmpty) - XCTAssertEqual(sut.errorMessage, ImageProcessError.noExtractedCoinID.description) + XCTAssertEqual(sut.errorMessage, ImageProcessError.noRecognizedText.description) } } diff --git a/AIProject/iCoTests/TextRecognitionHelperTests.swift b/AIProject/iCoTests/TextRecognitionHelperTests.swift index e8433865..5ef41f09 100644 --- a/AIProject/iCoTests/TextRecognitionHelperTests.swift +++ b/AIProject/iCoTests/TextRecognitionHelperTests.swift @@ -1,67 +1,64 @@ -//// -//// TextRecognitionHelperTests.swift -//// AIProjectTests -//// -//// Created by Kitcat Seo on 8/15/25. -//// // -//import XCTest -//import Vision -//@testable import iCo +// TextRecognitionHelperTests.swift +// AIProjectTests // -//// MARK: - TextRecognitionHelper 테스트 -//final class TextRecognitionHelperTests: XCTestCase { -// var sut: TextRecognitionHelper! -// -// var mockCoinList: Set! -// var mockImage: UIImage! -// -// override func setUp() async throws { -// try await super.setUp() -// mockCoinList = ["bitcoin"] -// } -// -// override func tearDown() async throws { -// sut = nil -// mockCoinList = nil -// mockImage = nil -// try await super.tearDown() -// } -// -// // test대상_작업_예상결과 -// // OCR 결과 텍스트에 코인 이름이 포함돼있을 때 결과를 정상적으로 반환하는지? -// func testHandleOCR_whenTextContainsCoinNames_returnsCoinList() async throws { -// // Given -// mockImage = ImageProcessTestHelpers.createTestImage(with: "sunrise maple orbit bitcoin velvet") -// sut = TextRecognitionHelper( -// image: mockImage, -// coinNames: mockCoinList -// ) -// -// // When -// let raw = try await sut.handleOCR() -// let results = raw.first!.components(separatedBy: " ") -// -// // Then -// XCTAssertFalse(results.isEmpty) -// XCTAssertTrue(results.contains("bitcoin")) -// } -// -// // OCR 결과 텍스트 중 코인 이름이 아닌 문자열에 정상적으로 마스킹이 실행되는지? -// func testHandleOCR_whenTextContainsNonCoinNamesOnly_returnsMaskingCharactorsOnly() async throws { -// // Given -// mockImage = ImageProcessTestHelpers.createTestImage(with: "bittcoin 배트코인") -// sut = TextRecognitionHelper( -// image: mockImage, -// coinNames: mockCoinList -// ) -// -// // When -// let ocrResult = try await sut.handleOCR() -// let allCharacters = ocrResult.joined() -// -// // Then -// let maskingCharacters: Set = ["*", " "] -// XCTAssertTrue(allCharacters.allSatisfy { maskingCharacters.contains($0) }) -// } -//} +// Created by Kitcat Seo on 8/15/25. +// + +import XCTest +import Vision +@testable import iCo + +// MARK: - TextRecognitionHelper 테스트 +final class TextRecognitionHelperTests: XCTestCase { + var sut: TextRecognitionHelper! + + var mockCoinList: Set! + var mockImage: CGImage! + + override func setUp() async throws { + try await super.setUp() + mockCoinList = ["bitcoin"] + } + + override func tearDown() async throws { + sut = nil + mockCoinList = nil + mockImage = nil + try await super.tearDown() + } + + // OCR 결과 텍스트에 코인 이름이 포함돼있을 때 결과를 정상적으로 반환하는지? + func testHandleOCR_whenTextContainsCoinNames_returnsCoinList() async throws { + // Given + guard let mockImage = ImageProcessTestHelpers.createTestImage(with: "sunrise maple orbit bitcoin velvet") else { + throw XCTSkip("이미지가 정상적으로 생성되지 않음") + } + sut = TextRecognitionHelper() + + // When + let raw = try await sut.handleOCR(from: mockImage, with: mockCoinList) + let results = raw.first!.components(separatedBy: " ") + + // Then + XCTAssertFalse(results.isEmpty) + XCTAssertTrue(results.contains("bitcoin")) + } + + // OCR 결과 텍스트 중 코인 이름이 아닌 문자열에 정상적으로 마스킹이 실행되는지? + func testHandleOCR_whenTextContainsNonCoinNamesOnly_returnsMaskingCharactorsOnly() async throws { + // Given + guard let mockImage = ImageProcessTestHelpers.createTestImage(with: "bittcoin 배트코인") else { + throw XCTSkip("이미지가 정상적으로 생성되지 않음") + } + sut = TextRecognitionHelper() + + // When + let ocrResult = try await sut.handleOCR(from: mockImage, with: mockCoinList) + let allCharacters = ocrResult.joined() + + // Then + let maskingCharacters: Set = ["*", " "] + XCTAssertTrue(allCharacters.allSatisfy { maskingCharacters.contains($0) }) + } +}