Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 100 additions & 152 deletions koin.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions koin.xcodeproj/xcshareddata/xcschemes/NotificationService.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EC692284302700A400EE26ED"
BuildableName = "koinUnitTests.xctest"
BlueprintName = "koinUnitTests"
ReferencedContainer = "container:koin.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
Expand Down
13 changes: 13 additions & 0 deletions koin.xcodeproj/xcshareddata/xcschemes/koin-stage.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "EC692284302700A400EE26ED"
BuildableName = "koinUnitTests.xctest"
BlueprintName = "koinUnitTests"
ReferencedContainer = "container:koin.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
Expand Down
19 changes: 5 additions & 14 deletions koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,13 @@
shouldAutocreateTestPlan = "YES">
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A001E2C32845091F00D6C310"
BuildableName = "koinTests.xctest"
BlueprintName = "koinTests"
ReferencedContainer = "container:koin.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "A001E2CD2845091F00D6C310"
BuildableName = "koinUITests.xctest"
BlueprintName = "koinUITests"
BlueprintIdentifier = "EC692284302700A400EE26ED"
BuildableName = "koinUnitTests.xctest"
BlueprintName = "koinUnitTests"
Comment on lines +34 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify whether the new test target requires an `@testable` import.
rg -n -C 2 '`@testable`[[:space:]]+import[[:space:]]+koin' koinUnitTests

# Compare the application Debug and Release testability settings.
sed -n '6747,6990p' koin.xcodeproj/project.pbxproj

Repository: BCSDLab/KOIN_iOS

Length of output: 11811


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

proj = Path("koin.xcodeproj/project.pbxproj").read_text()

configs = re.findall(r'(EC69228B302700A400EE26ED|EC69228C302700A400EE26ED|\w{24})\s*/\*\s*(Debug|Release)\s*\*/\s*=\s*\{\s*isa\s*=\s*XCBuildConfiguration;.*?name\s*=\s*(?:Debug|Release);\s*\};', proj, re.S)
for uuid, name in configs:
    content = re.search(fr'{re.escape(uuid)}\s*/\*\s*{name}\s*\*/\s*=.*?name\s*=\s*{name};\s*\};', proj, re.S).group(0)
    print(f"{uuid} {name} ENABLE_TESTABILITY=", "ENABLE_TESTABILITY = YES" in content)

test_schemes = []
for p in Path("koin.xcodeproj/xcshareddata/xcschemes").glob("*.xcscheme"):
    scheme = p.read_text()
    if "EC692284302700A400EE26ED" in scheme:
        m = re.search(r'<TestAction[^>]*buildConfiguration="([^"]+)"', scheme)
        test_schemes.append((p, m.group(1) if m else None, "`@testable` import koin" in scheme))

print("Test schemes:", test_schemes)

all_imports = []
for path in sorted(Path("koinUnitTests").rglob("*.swift")):
    all_imports.extend(path.read_text(errors="ignore").splitlines())
print("Files using `@testable` import koin:", sum(1 for line in all_imports if "`@testable` import koin" in line))
print("All references:", [f"{p}:{line_no}" for p in all_imports[:0]])
PY

Repository: BCSDLab/KOIN_iOS

Length of output: 447


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

proj = Path("koin.xcodeproj/project.pbxproj").read_text()

uuids = [
    "EC69228B302700A400EE26ED",
    "EC69228C302700A400EE26ED",
]
for uuid in uuids:
    m = re.search(rf'({re.escape(uuid)})\s*/\*\s*(Debug|Release)\s*\*/', proj)
    if not m:
        continue
    uuid, name = m.groups()
    full = re.search(rf'{re.escape(uuid)}\s*/\*\s*{re.escape(name)}\s*\*/\s*=.*?name\s*=\s*{re.escape(name)};\s*\};', proj, re.S).group(0)
    print(f"{uuid} {name} ENABLE_TESTABILITY=", "ENABLE_TESTABILITY = YES" in full)

test_schemes = []
schemes_dir = Path("koin.xcodeproj/xcshareddata/xcschemes")
if schemes_dir.exists():
    for p in sorted(schemes_dir.glob("*.xcscheme")):
        scheme = p.read_text()
        if "EC692284302700A400EE26ED" in scheme:
            mt = re.search(r'<TestAction[^>]*buildConfiguration="([^"]+)"', scheme)
            mi = re.search(r'`@testable`\s+import\s+koin', scheme)
            test_schemes.append((str(p), mt.group(1) if mt else None, bool(mi)))
print("Test schemes:", test_schemes)

all_imports = []
for path in sorted(Path("koinUnitTests").rglob("*.swift")):
    all_imports.extend(path.read_text(errors="ignore").splitlines())
print("Files using `@testable` import koin:", sum(1 for line in all_imports if "`@testable` import koin" in line))
PY

python3 - <<'PY'
from pathlib import Path

files = {}
for path in sorted(Path("koinUnitTests").rglob("*.swift")):
    text = path.read_text(errors="ignore").splitlines()
    hits = [(i+1, line.strip()) for i, line in enumerate(text) if "`@testable` import koin" in line]
    if hits:
        files[str(path)] = hits

print("koinUnitTests files with `@testable` import koin:")
for path, hits in files.items():
    print(path)
    for i, line in hits[:5]:
        print(f"  {i}: {line}")
    if len(hits) > 5:
        print(f"  ... and {len(hits)-5} more")
PY

Repository: BCSDLab/KOIN_iOS

Length of output: 486


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

proj = Path("koin.xcodeproj/project.pbxproj").read_text()

uuid_config_map = {
    "EC69228B302700A400EE26ED": "Debug",
    "EC69228C302700A400EE26ED": "Release",
}

for uuid, name in uuid_config_map.items():
    escaped_uuid = re.escape(uuid)
    escaped_name = re.escape(name)
    m = re.search(rf'{escaped_uuid}\s*/\*\s*{escaped_name}\s*\*/', proj)
    if not m:
        continue
    full = re.search(rf'{escaped_uuid}\s*/\*\s*{escaped_name}\s*\*/\s*=.*?name\s*=\s*{escaped_name};\s*\};', proj, re.S).group(0)
    has_setting = "ENABLE_TESTABILITY = YES" in full
    print(f"{uuid} {name} ENABLE_TESTABILITY= {has_setting}")

test_schemes = []
schemes_dir = Path("koin.xcodeproj/xcshareddata/xcschemes")
if schemes_dir.exists():
    for p in sorted(schemes_dir.glob("*.xcscheme")):
        scheme = p.read_text()
        if "EC692284302700A400EE26ED" in scheme:
            mt = re.search(r'<TestAction[^>]*buildConfiguration="([^"]+)"', scheme)
            mi = re.search(r'`@testable`\s+import\s+koin', scheme)
            test_schemes.append((str(p), mt.group(1) if mt else None, bool(mi)))
print("Test schemes:", test_schemes)

files = {}
for path in sorted(Path("koinUnitTests").rglob("*.swift")):
    text = path.read_text(errors="ignore").splitlines()
    hits = [(i+1, line.strip()) for i, line in enumerate(text) if "`@testable` import koin" in line]
    if hits:
        files[str(path)] = hits

print("koinUnitTests files with `@testable` import koin:", len(files))
for path, hits in list(files.items())[:5]:
    print(path)
    for i, line in hits[:5]:
        print(f"  {i}: {line}")
PY

Repository: BCSDLab/KOIN_iOS

Length of output: 468


Don’t run @testable tests from a Release scheme.

koinUnitTests uses @testable import koin, but the koin Release configuration does not enable testability. Use Debug for TestAction, or enable ENABLE_TESTABILITY = YES for koin only in the Release build path used by tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@koin.xcodeproj/xcshareddata/xcschemes/koin.xcscheme` around lines 34 - 40,
Update the koin scheme’s TestAction configuration so koinUnitTests runs against
the Debug configuration rather than Release, preserving testability for its
`@testable` import. Alternatively, enable ENABLE_TESTABILITY only for the koin
Release build path used by these tests.

ReferencedContainer = "container:koin.xcodeproj">
</BuildableReference>
</TestableReference>
Expand Down
41 changes: 0 additions & 41 deletions koinUITests/koinUITests.swift

This file was deleted.

32 changes: 0 additions & 32 deletions koinUITests/koinUITestsLaunchTests.swift

This file was deleted.

80 changes: 80 additions & 0 deletions koinUnitTests/Dining/DateProviderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// DateProviderTests.swift
// koinUnitTests
//
// Created by 이은지 on 8/8/26.
//

import Foundation
import Testing
@testable import koin

@Suite("DateProvider - 시간대 판단 로직 경계값")
struct DateProviderTests {

private let sut = DefaultDateProvider()
private let calendar = Calendar.current

private func date(hour: Int, minute: Int) throws -> Date {
try #require(DiningFixture.date(hour: hour, minute: minute, calendar: calendar))
}

@Test("09:00 이전이면 breakfast를 반환한다")
func 아홉시_이전이면_breakfast를_반환한다() throws {
let input = try date(hour: 8, minute: 59)

let result = sut.execute(date: input)

#expect(result.diningType == .breakfast)
#expect(calendar.isDate(result.date, inSameDayAs: input))
}

@Test("09:00이면 lunch를 반환한다")
func 아홉시면_lunch를_반환한다() throws {
let input = try date(hour: 9, minute: 0)

let result = sut.execute(date: input)

#expect(result.diningType == .lunch)
#expect(calendar.isDate(result.date, inSameDayAs: input))
}

@Test("13:30이면 lunch를 반환한다")
func 열세시_삼십분이면_lunch를_반환한다() throws {
let input = try date(hour: 13, minute: 30)

let result = sut.execute(date: input)

#expect(result.diningType == .lunch)
}

@Test("13:31이면 dinner를 반환한다")
func 열세시_삼십일분이면_dinner를_반환한다() throws {
let input = try date(hour: 13, minute: 31)

let result = sut.execute(date: input)

#expect(result.diningType == .dinner)
#expect(calendar.isDate(result.date, inSameDayAs: input))
}

@Test("18:30이면 dinner를 반환한다")
func 열여덟시_삼십분이면_dinner를_반환한다() throws {
let input = try date(hour: 18, minute: 30)

let result = sut.execute(date: input)

#expect(result.diningType == .dinner)
}

@Test("18:30 이후면 다음 날 breakfast를 반환한다", arguments: [(18, 31), (23, 59)])
func 열여덟시_삼십분_이후면_다음_날_breakfast를_반환한다(hour: Int, minute: Int) throws {
let input = try date(hour: hour, minute: minute)
let expectedNextDay = try #require(calendar.date(byAdding: .day, value: 1, to: input))

let result = sut.execute(date: input)

#expect(result.diningType == .breakfast)
#expect(calendar.isDate(result.date, inSameDayAs: expectedNextDay))
}
}
88 changes: 88 additions & 0 deletions koinUnitTests/Dining/FetchDiningListUseCaseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//
// FetchDiningListUseCaseTests.swift
// koinUnitTests
//
// Created by 이은지 on 8/8/26.
//

import Foundation
import Testing
@testable import koin

@Suite("FetchDiningListUseCase - segmentControl에 따른 데이터 필터링")
struct FetchDiningListUseCaseTests {

private func makeSUT(
stubbedDiningList: [DiningDto]
) -> (sut: DefaultFetchDiningListUseCase, spy: SpyDiningRepository) {
let spy = SpyDiningRepository()
spy.stubbedDiningList = stubbedDiningList
return (DefaultFetchDiningListUseCase(diningRepository: spy), spy)
}

private func diningInfo(
type: DiningType,
date: Date = Date()
) -> CurrentDiningTime {
CurrentDiningTime(date: date, diningType: type)
}

@Test("요청한 시간대의 식단만 반환한다")
func 요청한_시간대의_식단만_반환한다() async throws {
let (sut, _) = makeSUT(stubbedDiningList: [
DiningFixture.dto(id: 1, type: .breakfast, place: .cornerA),
DiningFixture.dto(id: 2, type: .lunch, place: .cornerB),
DiningFixture.dto(id: 3, type: .dinner, place: .cornerC),
DiningFixture.dto(id: 4, type: .lunch, place: .special)
])

let result = try await sut.execute(diningInfo: diningInfo(type: .lunch)).firstValue()

#expect(result.count == 2)
#expect(result.allSatisfy { $0.type == .lunch })
#expect(result.map(\.id).sorted() == [2, 4])
}

@Test(
"미운영 메뉴는 제외한다",
arguments: [DiningType.breakfast, .lunch, .dinner]
)
func 미운영_메뉴는_제외한다(requestedType: DiningType) async throws {
let (sut, _) = makeSUT(stubbedDiningList: [
DiningFixture.dto(id: 1, type: requestedType, place: .cornerA, menu: ["미운영"]),
DiningFixture.dto(id: 2, type: requestedType, place: .cornerB, menu: ["김치찌개", "밥"])
])

let result = try await sut.execute(diningInfo: diningInfo(type: requestedType)).firstValue()

#expect(result.count == 1)
#expect(result.first?.id == 2)
#expect(!result.contains { $0.menu.first == "미운영" })
}

@Test("장소 우선순위대로 정렬한다")
func 장소_우선순위대로_정렬한다() async throws {
let (sut, _) = makeSUT(stubbedDiningList: [
DiningFixture.dto(id: 1, type: .lunch, place: .secondCampus),
DiningFixture.dto(id: 2, type: .lunch, place: .special),
DiningFixture.dto(id: 3, type: .lunch, place: .cornerC),
DiningFixture.dto(id: 4, type: .lunch, place: .cornerA),
DiningFixture.dto(id: 5, type: .lunch, place: .cornerB)
])

let result = try await sut.execute(diningInfo: diningInfo(type: .lunch)).firstValue()

#expect(result.map(\.place) == [.cornerA, .cornerB, .cornerC, .special, .secondCampus])
}

@Test("요청 날짜를 yyMMdd 형식으로 전달한다")
func 요청_날짜를_yyMMdd_형식으로_전달한다() async throws {
let (sut, spy) = makeSUT(stubbedDiningList: [])
let requestedDate = try #require(DiningFixture.date(year: 2026, month: 8, day: 3))

_ = try await sut.execute(diningInfo: diningInfo(type: .lunch, date: requestedDate)).firstValue()

#expect(spy.fetchDiningListCallCount == 1)
#expect(spy.receivedFetchRequests.first?.date == "260803")
}
}
82 changes: 82 additions & 0 deletions koinUnitTests/Dining/ShareMenuListUseCaseTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//
// ShareMenuListUseCaseTests.swift
// koinUnitTests
//
// Created by 이은지 on 8/8/26.
//

import Foundation
import Testing
@testable import koin

@Suite("ShareMenuListUseCase - 카카오톡 식단 공유하기")
struct ShareMenuListUseCaseTests {

@Test("DiningItem을 ShareDiningMenu로 변환하면 메뉴와 이미지를 유지한다")
func DiningItem을_ShareDiningMenu로_변환하면_메뉴와_이미지를_유지한다() {
let item = DiningFixture.item(
type: .lunch,
place: .cornerA,
menu: ["김치찌개", "밥"],
imageUrl: "url1"
)

let shareModel = item.toShareDiningItem()

#expect(shareModel.menuList == ["김치찌개", "밥"])
#expect(shareModel.imageUrl == "url1")
#expect(shareModel.type == .lunch)
#expect(shareModel.place == .cornerA)
}

@Test("날짜를 yyMMdd 형식으로 변환한다")
func 날짜를_yyMMdd_형식으로_변환한다() {
let item = DiningFixture.item(date: "2026-08-03")

let shareModel = item.toShareDiningItem()

#expect(shareModel.date == "260803")
}

@Test(
"날짜 변환에 실패하면 원본 문자열을 사용한다",
arguments: ["", "날짜없음", "2026-13-45"]
)
func 날짜_변환에_실패하면_원본_문자열을_사용한다(invalidDate: String) {
let item = DiningFixture.item(date: invalidDate)

let shareModel = item.toShareDiningItem()

#expect(shareModel.date == invalidDate)
}

@Test("구분자가 달라도 파싱에 성공하면 yyMMdd로 변환된다")
func 구분자가_달라도_파싱에_성공하면_yyMMdd로_변환된다() {
let item = DiningFixture.item(date: "2026/08/03")

let shareModel = item.toShareDiningItem()

#expect(shareModel.date == "260803")
}

@Test("공유 모델을 레포지토리에 전달한다")
func 공유_모델을_레포지토리에_전달한다() throws {
let spy = SpyDiningRepository()
let sut = DefaultShareMenuListUseCase(diningRepository: spy)
let shareModel = DiningFixture.item(
date: "2026-08-03",
place: .cornerB,
menu: ["돈까스"],
imageUrl: "url2"
).toShareDiningItem()

sut.execute(shareModel: shareModel)

#expect(spy.receivedShareModels.count == 1)
let received = try #require(spy.receivedShareModels.first)
#expect(received.menuList == shareModel.menuList)
#expect(received.imageUrl == shareModel.imageUrl)
#expect(received.date == shareModel.date)
#expect(received.place == shareModel.place)
}
}
Loading