From 8ba2766b83aea8e21509ae02e8b55b81be7c5751 Mon Sep 17 00:00:00 2001 From: daikiumehara Date: Fri, 26 Apr 2024 19:43:33 +0900 Subject: [PATCH 01/24] edit: rename callbackFetchWeatherList to fetchWeather --- Sources/YumemiWeather/YumemiWeather.swift | 4 ++-- Sources/YumemiWeather/YumemiWeatherList.swift | 4 ++-- Tests/YumemiWeatherTests/YumemiWeatherTests.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index e14c735..9dd8f33 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -183,7 +183,7 @@ final public class YumemiWeather { /// - Parameters: /// - jsonString: 地域と日付を含む JSON 文字列 /// - completion: 完了コールバック - public static func callbackFetchWeather(_ jsonString: String, completion: @escaping (Result) -> Void) { + public static func fetchWeather(_ jsonString: String, completion: @escaping (Result) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + apiDuration) { do { let response = try fetchWeather(jsonString) @@ -222,7 +222,7 @@ final public class YumemiWeather { @available(iOS 13, macOS 10.15, *) public static func asyncFetchWeather(_ jsonString: String) async throws -> String { return try await withCheckedThrowingContinuation { continuation in - callbackFetchWeather(jsonString) { result in + fetchWeather(jsonString) { result in continuation.resume(with: result) } } diff --git a/Sources/YumemiWeather/YumemiWeatherList.swift b/Sources/YumemiWeather/YumemiWeatherList.swift index 0c7f87e..cd4b9a7 100644 --- a/Sources/YumemiWeather/YumemiWeatherList.swift +++ b/Sources/YumemiWeather/YumemiWeatherList.swift @@ -136,7 +136,7 @@ public extension YumemiWeather { /// - Parameters: /// - jsonString: 地域と日付を含む JSON 文字列 /// - completion: 完了コールバック - static func callbackFetchWeatherList(_ jsonString: String, completion: @escaping (Result) -> Void) { + static func fetchWeatherList(_ jsonString: String, completion: @escaping (Result) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + apiDuration) { do { let response = try fetchWeatherList(jsonString) @@ -178,7 +178,7 @@ public extension YumemiWeather { @available(iOS 13, macOS 10.15, *) static func asyncFetchWeatherList(_ jsonString: String) async throws -> String { return try await withCheckedThrowingContinuation { continuation in - callbackFetchWeatherList(jsonString) { result in + fetchWeatherList(jsonString) { result in continuation.resume(with: result) } } diff --git a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift index 0392a4d..a826e77 100644 --- a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift +++ b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift @@ -80,7 +80,7 @@ final class YumemiWeatherTests: XCTestCase { } """ let exp = expectation(description: #function) - YumemiWeather.callbackFetchWeather(parameter) { result in + YumemiWeather.fetchWeather(parameter) { result in exp.fulfill() switch result { case .success(let jsonString): From 9520e85b9a53f9de92e048ba444400628252c1f2 Mon Sep 17 00:00:00 2001 From: daikiumehara Date: Fri, 26 Apr 2024 19:59:48 +0900 Subject: [PATCH 02/24] edit: remove unnecessary makeResponse code --- Sources/YumemiWeather/YumemiWeather.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 9dd8f33..e4529a5 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -55,12 +55,14 @@ final public class YumemiWeather { /// - seed: シード値 /// - Returns: Response構造体 - static func makeRandomResponse(weatherCondition: WeatherCondition? = nil, maxTemperature: Int? = nil, minTemperature: Int? = nil, date: Date? = nil, seed: Int? = nil) -> Response { - return makeRandomResponse(weatherCondition: weatherCondition, maxTemperature: maxTemperature, minTemperature: minTemperature, date: date, seed: seed ?? Int.random(in: Int.min...Int.max)) - } - - private static func makeRandomResponse(weatherCondition: WeatherCondition?, maxTemperature: Int?, minTemperature: Int?, date: Date?, seed seedValue: Int) -> Response { - var generator = SeedRandomNumberGenerator(seed: seedValue) + static func makeRandomResponse( + weatherCondition: WeatherCondition? = nil, + maxTemperature: Int? = nil, + minTemperature: Int? = nil, + date: Date? = nil, + seed: Int? = nil + ) -> Response { + var generator = SeedRandomNumberGenerator(seed: seed ?? Int.random(in: Int.min...Int.max)) let weatherCondition = weatherCondition ?? WeatherCondition.allCases.randomElement(using: &generator)! let maxTemperature = maxTemperature ?? Int.random(in: 10...40, using: &generator) let minTemperature = minTemperature ?? Int.random(in: -40.. Date: Tue, 30 Apr 2024 11:16:19 +0900 Subject: [PATCH 03/24] edit: refactor casting process --- Sources/YumemiWeather/YumemiWeather.swift | 6 +++--- Sources/YumemiWeather/YumemiWeatherList.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index e4529a5..31a5a9a 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -189,10 +189,10 @@ final public class YumemiWeather { DispatchQueue.global().asyncAfter(deadline: .now() + apiDuration) { do { let response = try fetchWeather(jsonString) - completion(Result.success(response)) + completion(.success(response)) } - catch let error where error is YumemiWeatherError { - completion(Result.failure(error as! YumemiWeatherError)) + catch let error as YumemiWeatherError { + completion(.failure(error)) } catch { fatalError() diff --git a/Sources/YumemiWeather/YumemiWeatherList.swift b/Sources/YumemiWeather/YumemiWeatherList.swift index cd4b9a7..44ced8c 100644 --- a/Sources/YumemiWeather/YumemiWeatherList.swift +++ b/Sources/YumemiWeather/YumemiWeatherList.swift @@ -140,10 +140,10 @@ public extension YumemiWeather { DispatchQueue.global().asyncAfter(deadline: .now() + apiDuration) { do { let response = try fetchWeatherList(jsonString) - completion(Result.success(response)) + completion(.success(response)) } - catch let error where error is YumemiWeatherError { - completion(Result.failure(error as! YumemiWeatherError)) + catch let error as YumemiWeatherError { + completion(.failure(error)) } catch { fatalError() From ebd0419d1675d17fcf3d4bb8ed824e66fd29070c Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Tue, 30 Apr 2024 23:12:05 +0900 Subject: [PATCH 04/24] =?UTF-8?q?Example=20=E3=81=AE=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=B3=E3=83=BC=E3=83=89=E3=81=A7=20WeatherModel=20?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=88=E3=82=B3=E3=83=AB=E3=81=AE=E8=A6=81?= =?UTF-8?q?=E6=B1=82=E3=82=92=E6=BA=80=E3=81=9F=E3=81=9B=E3=81=A6=E3=81=84?= =?UTF-8?q?=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E3=81=AE=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WeatherViewControllerTests.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Example/ExampleTests/WeatherViewControllerTests.swift b/Example/ExampleTests/WeatherViewControllerTests.swift index 402273d..498c9c5 100644 --- a/Example/ExampleTests/WeatherViewControllerTests.swift +++ b/Example/ExampleTests/WeatherViewControllerTests.swift @@ -28,7 +28,8 @@ class WeatherViewControllerTests: XCTestCase { func test_天気予報がsunnyだったらImageViewのImageにsunnyが設定されること_TintColorがredに設定されること() throws { weahterModel.fetchWeatherImpl = { _ in - Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date()) + let response = Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date()) + return Result.success(response) } weahterViewController.loadWeather() @@ -38,7 +39,8 @@ class WeatherViewControllerTests: XCTestCase { func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() throws { weahterModel.fetchWeatherImpl = { _ in - Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date()) + let response = Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date()) + return Result.success(response) } weahterViewController.loadWeather() @@ -48,7 +50,8 @@ class WeatherViewControllerTests: XCTestCase { func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() throws { weahterModel.fetchWeatherImpl = { _ in - Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date()) + let response = Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date()) + return Result.success(response) } weahterViewController.loadWeather() @@ -58,7 +61,8 @@ class WeatherViewControllerTests: XCTestCase { func test_最高気温_最低気温がUILabelに設定されること() throws { weahterModel.fetchWeatherImpl = { _ in - Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date()) + let response = Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date()) + return Result.success(response) } weahterViewController.loadWeather() @@ -69,9 +73,9 @@ class WeatherViewControllerTests: XCTestCase { class WeatherModelMock: WeatherModel { - var fetchWeatherImpl: ((Request) throws -> Response)! + var fetchWeatherImpl: ((Request) -> Result)! - func fetchWeather(_ request: Request) throws -> Response { - return try fetchWeatherImpl(request) + func fetchWeather(at area: String, date: Date, completion: @escaping (Result) -> Void) { + completion(fetchWeatherImpl(Request(area: area, date: date))) } } From 50276c9dbb0d4f2260bbf5bb7d2386babec5ab6d Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Tue, 30 Apr 2024 23:23:07 +0900 Subject: [PATCH 05/24] =?UTF-8?q?Example=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E5=86=85=E3=81=A7=E3=80=81loadWeath?= =?UTF-8?q?er=20=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E3=81=AB=E3=81=9D?= =?UTF-8?q?=E3=81=AE=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E4=B8=BB=E3=81=8C?= =?UTF-8?q?=E6=B8=A1=E3=81=95=E3=82=8C=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97=E3=81=BE=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Example/ExampleTests/WeatherViewControllerTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Example/ExampleTests/WeatherViewControllerTests.swift b/Example/ExampleTests/WeatherViewControllerTests.swift index 498c9c5..ba737ca 100644 --- a/Example/ExampleTests/WeatherViewControllerTests.swift +++ b/Example/ExampleTests/WeatherViewControllerTests.swift @@ -32,7 +32,7 @@ class WeatherViewControllerTests: XCTestCase { return Result.success(response) } - weahterViewController.loadWeather() + weahterViewController.loadWeather(self) XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.red()) XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.sunny()) } @@ -43,7 +43,7 @@ class WeatherViewControllerTests: XCTestCase { return Result.success(response) } - weahterViewController.loadWeather() + weahterViewController.loadWeather(self) XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.gray()) XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.cloudy()) } @@ -54,7 +54,7 @@ class WeatherViewControllerTests: XCTestCase { return Result.success(response) } - weahterViewController.loadWeather() + weahterViewController.loadWeather(self) XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.blue()) XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.rainy()) } @@ -65,7 +65,7 @@ class WeatherViewControllerTests: XCTestCase { return Result.success(response) } - weahterViewController.loadWeather() + weahterViewController.loadWeather(self) XCTAssertEqual(weahterViewController.minTempLabel.text, "-100") XCTAssertEqual(weahterViewController.maxTempLabel.text, "100") } From 70f228f8c246e2e2c071aacc9e156f9243de690b Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Tue, 30 Apr 2024 23:38:05 +0900 Subject: [PATCH 06/24] =?UTF-8?q?WeatherViewController=20=E3=81=AB=20Disas?= =?UTF-8?q?terModelMock=20=E3=81=8C=E8=A8=AD=E5=AE=9A=E3=81=95=E3=82=8C?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=81=93=E3=81=A8=E3=81=8C?= =?UTF-8?q?=E5=8E=9F=E5=9B=A0=E3=81=A7=E3=80=81=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=8C=E5=BC=B7=E5=88=B6=E7=B5=82=E4=BA=86=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97=E3=81=BE=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Example/ExampleTests/WeatherViewControllerTests.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Example/ExampleTests/WeatherViewControllerTests.swift b/Example/ExampleTests/WeatherViewControllerTests.swift index ba737ca..1839910 100644 --- a/Example/ExampleTests/WeatherViewControllerTests.swift +++ b/Example/ExampleTests/WeatherViewControllerTests.swift @@ -19,6 +19,8 @@ class WeatherViewControllerTests: XCTestCase { weahterModel = WeatherModelMock() weahterViewController = R.storyboard.weather.instantiateInitialViewController()! weahterViewController.weatherModel = weahterModel + weahterViewController.disasterModel = DisasterModelMock() + _ = weahterViewController.view } @@ -79,3 +81,10 @@ class WeatherModelMock: WeatherModel { completion(fetchWeatherImpl(Request(area: area, date: date))) } } + +final class DisasterModelMock: DisasterModel { + + func fetchDisaster(completion: ((String) -> Void)?) { + completion?("只今、災害情報はありません。") + } +} From 2d2da356e78adf5a0a18de5d246f991faa20d08f Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 00:01:37 +0900 Subject: [PATCH 07/24] =?UTF-8?q?WeatherImageView=20=E3=81=B8=E3=81=AE?= =?UTF-8?q?=E5=8F=8D=E6=98=A0=E3=82=92=E5=BE=85=E3=81=9F=E3=81=9A=E3=81=AB?= =?UTF-8?q?=20XCTAssertEqual=20=E3=81=A7=E7=B5=90=E6=9E=9C=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E3=82=92=E3=81=97=E3=81=A6=E5=A4=B1=E6=95=97=E3=81=97?= =?UTF-8?q?=E3=81=A6=E3=81=84=E3=81=9F=E7=AE=87=E6=89=80=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 現状の WeatherViewController には反映完了を検出する術がなさそうだったため、やや強引かつ不確実なテストコードになりました。 --- .../WeatherViewControllerTests.swift | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Example/ExampleTests/WeatherViewControllerTests.swift b/Example/ExampleTests/WeatherViewControllerTests.swift index 1839910..15ee7a3 100644 --- a/Example/ExampleTests/WeatherViewControllerTests.swift +++ b/Example/ExampleTests/WeatherViewControllerTests.swift @@ -28,46 +28,58 @@ class WeatherViewControllerTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func test_天気予報がsunnyだったらImageViewのImageにsunnyが設定されること_TintColorがredに設定されること() throws { + @MainActor + func test_天気予報がsunnyだったらImageViewのImageにsunnyが設定されること_TintColorがredに設定されること() async throws { weahterModel.fetchWeatherImpl = { _ in let response = Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date()) return Result.success(response) } weahterViewController.loadWeather(self) + await Task.yield() + XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.red()) XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.sunny()) } - func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() throws { + @MainActor + func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() async throws { weahterModel.fetchWeatherImpl = { _ in let response = Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date()) return Result.success(response) } weahterViewController.loadWeather(self) + await Task.yield() + XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.gray()) XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.cloudy()) } - func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() throws { + @MainActor + func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() async throws { weahterModel.fetchWeatherImpl = { _ in let response = Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date()) return Result.success(response) } weahterViewController.loadWeather(self) + await Task.yield() + XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.blue()) XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.rainy()) } - func test_最高気温_最低気温がUILabelに設定されること() throws { + @MainActor + func test_最高気温_最低気温がUILabelに設定されること() async throws { weahterModel.fetchWeatherImpl = { _ in let response = Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date()) return Result.success(response) } weahterViewController.loadWeather(self) + await Task.yield() + XCTAssertEqual(weahterViewController.minTempLabel.text, "-100") XCTAssertEqual(weahterViewController.maxTempLabel.text, "100") } From f8d13aee5cc6eda642324deb89ae2229e35d0d74 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 00:06:28 +0900 Subject: [PATCH 08/24] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E5=86=85=E3=81=AE=20typo=20=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WeatherViewControllerTests.swift | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Example/ExampleTests/WeatherViewControllerTests.swift b/Example/ExampleTests/WeatherViewControllerTests.swift index 15ee7a3..fe40aa3 100644 --- a/Example/ExampleTests/WeatherViewControllerTests.swift +++ b/Example/ExampleTests/WeatherViewControllerTests.swift @@ -12,16 +12,16 @@ import YumemiWeather class WeatherViewControllerTests: XCTestCase { - var weahterViewController: WeatherViewController! - var weahterModel: WeatherModelMock! + var weatherViewController: WeatherViewController! + var weatherModel: WeatherModelMock! override func setUpWithError() throws { - weahterModel = WeatherModelMock() - weahterViewController = R.storyboard.weather.instantiateInitialViewController()! - weahterViewController.weatherModel = weahterModel - weahterViewController.disasterModel = DisasterModelMock() + weatherModel = WeatherModelMock() + weatherViewController = R.storyboard.weather.instantiateInitialViewController()! + weatherViewController.weatherModel = weatherModel + weatherViewController.disasterModel = DisasterModelMock() - _ = weahterViewController.view + _ = weatherViewController.view } override func tearDownWithError() throws { @@ -30,58 +30,58 @@ class WeatherViewControllerTests: XCTestCase { @MainActor func test_天気予報がsunnyだったらImageViewのImageにsunnyが設定されること_TintColorがredに設定されること() async throws { - weahterModel.fetchWeatherImpl = { _ in + weatherModel.fetchWeatherImpl = { _ in let response = Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date()) return Result.success(response) } - weahterViewController.loadWeather(self) + weatherViewController.loadWeather(self) await Task.yield() - XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.red()) - XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.sunny()) + XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.red()) + XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.sunny()) } @MainActor func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() async throws { - weahterModel.fetchWeatherImpl = { _ in + weatherModel.fetchWeatherImpl = { _ in let response = Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date()) return Result.success(response) } - weahterViewController.loadWeather(self) + weatherViewController.loadWeather(self) await Task.yield() - XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.gray()) - XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.cloudy()) + XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.gray()) + XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.cloudy()) } @MainActor func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() async throws { - weahterModel.fetchWeatherImpl = { _ in + weatherModel.fetchWeatherImpl = { _ in let response = Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date()) return Result.success(response) } - weahterViewController.loadWeather(self) + weatherViewController.loadWeather(self) await Task.yield() - XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.blue()) - XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.rainy()) + XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.blue()) + XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.rainy()) } @MainActor func test_最高気温_最低気温がUILabelに設定されること() async throws { - weahterModel.fetchWeatherImpl = { _ in + weatherModel.fetchWeatherImpl = { _ in let response = Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date()) return Result.success(response) } - weahterViewController.loadWeather(self) + weatherViewController.loadWeather(self) await Task.yield() - XCTAssertEqual(weahterViewController.minTempLabel.text, "-100") - XCTAssertEqual(weahterViewController.maxTempLabel.text, "100") + XCTAssertEqual(weatherViewController.minTempLabel.text, "-100") + XCTAssertEqual(weatherViewController.maxTempLabel.text, "100") } } From 81f35eb4d3682bb56c2541f97f883878aa6bf8b9 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 00:08:27 +0900 Subject: [PATCH 09/24] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=A7=E3=81=AE=20View=20=E3=81=AE?= =?UTF-8?q?=E6=BA=96=E5=82=99=E3=81=AF=E3=80=81=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=81=B8=E3=81=AE=E3=83=97=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3?= =?UTF-8?q?=E3=83=BC=E3=82=A2=E3=82=AF=E3=82=BB=E3=82=B9=E3=82=92=E6=8D=A8?= =?UTF-8?q?=E3=81=A6=E3=82=8B=E6=96=B9=E6=B3=95=E3=81=A7=E3=81=AF=E3=81=AA?= =?UTF-8?q?=E3=81=8F=20'loadViewIfNeeded`=20=E3=82=92=E5=91=BC=E3=81=B3?= =?UTF-8?q?=E5=87=BA=E3=81=99=E6=96=B9=E6=B3=95=E3=81=AB=E3=81=97=E3=81=BE?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Example/ExampleTests/WeatherViewControllerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Example/ExampleTests/WeatherViewControllerTests.swift b/Example/ExampleTests/WeatherViewControllerTests.swift index fe40aa3..29631bf 100644 --- a/Example/ExampleTests/WeatherViewControllerTests.swift +++ b/Example/ExampleTests/WeatherViewControllerTests.swift @@ -21,7 +21,7 @@ class WeatherViewControllerTests: XCTestCase { weatherViewController.weatherModel = weatherModel weatherViewController.disasterModel = DisasterModelMock() - _ = weatherViewController.view + weatherViewController.loadViewIfNeeded() } override func tearDownWithError() throws { From c1fb6be5ab665113fd940afe708b7d0161484d91 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 00:10:51 +0900 Subject: [PATCH 10/24] =?UTF-8?q?=E5=B0=91=E3=81=AA=E3=81=8F=E3=81=A8?= =?UTF-8?q?=E3=82=82=E7=8F=BE=E7=8A=B6=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E3=81=A7=E3=81=AF=20WeatherModelMoc?= =?UTF-8?q?k=20=E3=82=92=20'setUpWithError'=20=E3=81=BE=E3=81=A7=E5=BE=85?= =?UTF-8?q?=E3=81=9F=E3=81=AA=E3=81=8F=E3=81=A6=E3=82=82=E6=BA=96=E5=82=99?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=81=9F=E3=82=81=E3=80=81=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E3=81=AB=20IUO=20=E3=82=AA=E3=83=97=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=8A=E3=83=AB=E3=82=92=E4=BD=BF=E3=82=8F=E3=81=AA?= =?UTF-8?q?=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=E3=81=97=E3=81=BE=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Example/ExampleTests/WeatherViewControllerTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Example/ExampleTests/WeatherViewControllerTests.swift b/Example/ExampleTests/WeatherViewControllerTests.swift index 29631bf..5d9018c 100644 --- a/Example/ExampleTests/WeatherViewControllerTests.swift +++ b/Example/ExampleTests/WeatherViewControllerTests.swift @@ -13,10 +13,9 @@ import YumemiWeather class WeatherViewControllerTests: XCTestCase { var weatherViewController: WeatherViewController! - var weatherModel: WeatherModelMock! + var weatherModel = WeatherModelMock() override func setUpWithError() throws { - weatherModel = WeatherModelMock() weatherViewController = R.storyboard.weather.instantiateInitialViewController()! weatherViewController.weatherModel = weatherModel weatherViewController.disasterModel = DisasterModelMock() From acb3ddaf861d8350be2157f634a75fa2bfa69342 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 00:56:04 +0900 Subject: [PATCH 11/24] =?UTF-8?q?=E5=8D=98=E7=8B=AC=E3=81=A7=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E3=81=97=E3=81=A6=E3=81=84=E3=81=9F=E3=80=81=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E3=81=AA=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 31a5a9a..eec01b4 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -157,11 +157,6 @@ final public class YumemiWeather { Thread.sleep(forTimeInterval: apiDuration) return try self.fetchWeather(jsonString) } - - - /// - Throws: YumemiWeatherError パラメータが正常でもランダムにエラーが発生する - /// - Parameter jsonString: 地域と日付を含む JSON 文字列 - /// - Returns: Weather レスポンスの JSON 文字列 /// 擬似 天気予報 API Callback ver /// From ee6cf6d35120fe58107ac2174d9ef730ca3c51ca Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 02:19:54 +0900 Subject: [PATCH 12/24] =?UTF-8?q?self.=20=E3=81=AF=E7=9C=81=E7=95=A5?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=82=8B=E5=82=BE=E5=90=91=E3=81=8C=E3=81=82?= =?UTF-8?q?=E3=82=8B=E3=81=9F=E3=82=81=E3=80=81=E3=81=9D=E3=82=8C=E3=81=8C?= =?UTF-8?q?=E6=98=8E=E3=82=89=E3=81=8B=E3=81=AB=E4=B8=8D=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E7=AE=87=E6=89=80=E3=81=AB=E3=81=A4=E3=81=84=E3=81=A6=E3=81=AF?= =?UTF-8?q?=E7=9C=81=E7=95=A5=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 省略推奨についての確かな根拠は見つけられませんでした。以前に The Swift Programming Language で「通常は省略する」という記載があった気がしたのですけれど、探しても見つけられなかったため、省略しない方針もアリかもしれません。その場合はこのコミットを Revert しましょう。 --- Example/Example/Model/DisasterModel.swift | 6 ++--- .../UI/Weather/WeatherViewController.swift | 26 +++++++++---------- Sources/YumemiWeather/YumemiDisaster.swift | 2 +- Sources/YumemiWeather/YumemiWeather.swift | 6 ++--- Sources/YumemiWeather/YumemiWeatherList.swift | 2 +- .../YumemiWeatherTests.swift | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Example/Example/Model/DisasterModel.swift b/Example/Example/Model/DisasterModel.swift index 8dc5e03..eb1c190 100644 --- a/Example/Example/Model/DisasterModel.swift +++ b/Example/Example/Model/DisasterModel.swift @@ -20,15 +20,15 @@ class DisasterModelImpl: DisasterModel { } func fetchDisaster(completion: ((String) -> Void)?) { - self.fetchDisasterHandler = completion - self.yumemiDisaster.fetchDisaster() + fetchDisasterHandler = completion + yumemiDisaster.fetchDisaster() } } extension DisasterModelImpl: YumemiDisasterHandleDelegate { func handle(disaster: String) { - self.fetchDisasterHandler?(disaster) + fetchDisasterHandler?(disaster) } } diff --git a/Example/Example/UI/Weather/WeatherViewController.swift b/Example/Example/UI/Weather/WeatherViewController.swift index 0bde51d..68c8822 100644 --- a/Example/Example/UI/Weather/WeatherViewController.swift +++ b/Example/Example/UI/Weather/WeatherViewController.swift @@ -30,7 +30,7 @@ class WeatherViewController: UIViewController { super.viewDidLoad() NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [unowned self] notification in - self.loadWeather(notification.object) + loadWeather(notification.object) } } @@ -39,11 +39,11 @@ class WeatherViewController: UIViewController { } @IBAction func dismiss(_ sender: Any) { - self.dismiss(animated: true, completion: nil) + dismiss(animated: true, completion: nil) } @IBAction func loadWeather(_ sender: Any?) { - self.activityIndicator.startAnimating() + activityIndicator.startAnimating() weatherModel.fetchWeather(at: "tokyo", date: Date()) { result in DispatchQueue.main.async { self.activityIndicator.stopAnimating() @@ -58,9 +58,9 @@ class WeatherViewController: UIViewController { func handleWeather(result: Result) { switch result { case .success(let response): - self.weatherImageView.set(weather: response.weather) - self.minTempLabel.text = String(response.minTemp) - self.maxTempLabel.text = String(response.maxTemp) + weatherImageView.set(weather: response.weather) + minTempLabel.text = String(response.minTemp) + maxTempLabel.text = String(response.maxTemp) case .failure(let error): let message: String @@ -79,7 +79,7 @@ class WeatherViewController: UIViewController { print("Close ViewController by \(alertController)") } }) - self.present(alertController, animated: true, completion: nil) + present(alertController, animated: true, completion: nil) } } } @@ -88,14 +88,14 @@ private extension UIImageView { func set(weather: Weather) { switch weather { case .sunny: - self.image = R.image.sunny() - self.tintColor = R.color.red() + image = R.image.sunny() + tintColor = R.color.red() case .cloudy: - self.image = R.image.cloudy() - self.tintColor = R.color.gray() + image = R.image.cloudy() + tintColor = R.color.gray() case .rainy: - self.image = R.image.rainy() - self.tintColor = R.color.blue() + image = R.image.rainy() + tintColor = R.color.blue() } } } diff --git a/Sources/YumemiWeather/YumemiDisaster.swift b/Sources/YumemiWeather/YumemiDisaster.swift index 7af4390..84fdbc0 100644 --- a/Sources/YumemiWeather/YumemiDisaster.swift +++ b/Sources/YumemiWeather/YumemiDisaster.swift @@ -18,7 +18,7 @@ public class YumemiDisaster { public init() {} public func fetchDisaster() { - self.delegate?.handle(disaster: "只今、災害情報はありません。") + delegate?.handle(disaster: "只今、災害情報はありません。") } } diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index eec01b4..3c95dc3 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -79,7 +79,7 @@ final public class YumemiWeather { /// 擬似 天気予報 API Simple ver /// - Returns: 天気状況を表す文字列 "sunny" or "cloudy" or "rainy" public static func fetchWeatherCondition() -> String { - return self.makeRandomResponse().weatherCondition + return makeRandomResponse().weatherCondition } /// 擬似 天気予報 API Throws ver @@ -92,7 +92,7 @@ final public class YumemiWeather { throw YumemiWeatherError.unknownError } - return self.makeRandomResponse().weatherCondition + return makeRandomResponse().weatherCondition } /// 擬似 天気予報 API JSON ver @@ -155,7 +155,7 @@ final public class YumemiWeather { /// - Returns: Weather レスポンスの JSON 文字列 public static func syncFetchWeather(_ jsonString: String) throws -> String { Thread.sleep(forTimeInterval: apiDuration) - return try self.fetchWeather(jsonString) + return try fetchWeather(jsonString) } /// 擬似 天気予報 API Callback ver diff --git a/Sources/YumemiWeather/YumemiWeatherList.swift b/Sources/YumemiWeather/YumemiWeatherList.swift index 44ced8c..9ff9902 100644 --- a/Sources/YumemiWeather/YumemiWeatherList.swift +++ b/Sources/YumemiWeather/YumemiWeatherList.swift @@ -106,7 +106,7 @@ public extension YumemiWeather { /// - Returns: 返された AreaResponse の JSON 文字列 static func syncFetchWeatherList(_ jsonString: String) throws -> String { Thread.sleep(forTimeInterval: apiDuration) - return try self.fetchWeatherList(jsonString) + return try fetchWeatherList(jsonString) } /// 擬似 天気予報一覧 API Callback ver diff --git a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift index a826e77..322a95c 100644 --- a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift +++ b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift @@ -94,7 +94,7 @@ final class YumemiWeatherTests: XCTestCase { XCTAssertEqual(error, YumemiWeatherError.unknownError) } } - self.wait(for: [exp], timeout: YumemiWeather.apiDuration + 0.1) + wait(for: [exp], timeout: YumemiWeather.apiDuration + 0.1) } @available(iOS 13, macOS 10.15, *) From 17daa5f479753064eae3737dd9cd9d20c1279b6b Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 21:54:38 +0900 Subject: [PATCH 13/24] =?UTF-8?q?ThreadBlock=20=E3=81=AE=E8=AA=B2=E9=A1=8C?= =?UTF-8?q?=E6=96=87=E3=81=AB=E3=80=81=E4=BD=BF=E7=94=A8=E3=81=99=E3=82=8B?= =?UTF-8?q?=20API=20=E3=81=AE=E7=89=B9=E5=BE=B4=E8=AA=AC=E6=98=8E=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E8=A8=98=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Documentation/ThreadBlock.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Documentation/ThreadBlock.md b/Documentation/ThreadBlock.md index 7997067..f05909f 100644 --- a/Documentation/ThreadBlock.md +++ b/Documentation/ThreadBlock.md @@ -6,6 +6,9 @@ Sync ver ```swift static func syncFetchWeather(_ jsonString: String) throws -> String ``` + +このメソッドは、値を返すまでに少し時間がかかります。 + [APIの概要](YumemiWeather.md) ## 課題 From b7bf09c2ca7c43546ee5a7374e55be2b54b7c7b3 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Wed, 1 May 2024 21:57:18 +0900 Subject: [PATCH 14/24] =?UTF-8?q?YumemiWeather=20=E3=82=AF=E3=83=A9?= =?UTF-8?q?=E3=82=B9=E3=81=AE=E8=A6=8B=E9=80=9A=E3=81=97=E3=82=92=E8=89=AF?= =?UTF-8?q?=E3=81=8F=E3=81=99=E3=82=8B=E3=81=9F=E3=82=81=E3=81=AB=E3=80=81?= =?UTF-8?q?=E5=A4=96=E9=83=A8=E5=85=AC=E9=96=8B=E3=81=99=E3=82=8B=20API=20?= =?UTF-8?q?=E3=81=AE=E6=A9=9F=E8=83=BD=E3=81=A8=E3=81=9D=E3=82=8C=E4=BB=A5?= =?UTF-8?q?=E5=A4=96=E3=81=A8=E3=82=92=E5=8C=BA=E5=88=A5=E3=81=97=E3=81=A6?= =?UTF-8?q?=E5=AE=9A=E7=BE=A9=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 105 +++++++++++----------- 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 3c95dc3..18b97d1 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -25,57 +25,6 @@ public enum YumemiWeatherError: Swift.Error { final public class YumemiWeather { - static let apiDuration: TimeInterval = 2 - - private static let dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - return dateFormatter - }() - - static let decoder: JSONDecoder = { - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = .formatted(dateFormatter) - return decoder - }() - - static let encoder: JSONEncoder = { - let encoder = JSONEncoder() - encoder.keyEncodingStrategy = .convertToSnakeCase - encoder.dateEncodingStrategy = .formatted(dateFormatter) - return encoder - }() - - /// 引数の値でResponse構造体を作成する。引数がnilの場合はランダムに値を作成する。 - /// - Parameters: - /// - weatherCondition: 天気状況を表すenum - /// - maxTemperature: 最高気温 - /// - minTemperature: 最低気温 - /// - date: 日付 - /// - seed: シード値 - /// - Returns: Response構造体 - - static func makeRandomResponse( - weatherCondition: WeatherCondition? = nil, - maxTemperature: Int? = nil, - minTemperature: Int? = nil, - date: Date? = nil, - seed: Int? = nil - ) -> Response { - var generator = SeedRandomNumberGenerator(seed: seed ?? Int.random(in: Int.min...Int.max)) - let weatherCondition = weatherCondition ?? WeatherCondition.allCases.randomElement(using: &generator)! - let maxTemperature = maxTemperature ?? Int.random(in: 10...40, using: &generator) - let minTemperature = minTemperature ?? Int.random(in: -40.. String { @@ -225,3 +174,57 @@ final public class YumemiWeather { } } } + +extension YumemiWeather { + + static let apiDuration: TimeInterval = 2 + + private static let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + return dateFormatter + }() + + static let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(dateFormatter) + return decoder + }() + + static let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + encoder.dateEncodingStrategy = .formatted(dateFormatter) + return encoder + }() + + /// 引数の値でResponse構造体を作成する。引数がnilの場合はランダムに値を作成する。 + /// - Parameters: + /// - weatherCondition: 天気状況を表すenum + /// - maxTemperature: 最高気温 + /// - minTemperature: 最低気温 + /// - date: 日付 + /// - seed: シード値 + /// - Returns: Response構造体 + + static func makeRandomResponse( + weatherCondition: WeatherCondition? = nil, + maxTemperature: Int? = nil, + minTemperature: Int? = nil, + date: Date? = nil, + seed: Int? = nil + ) -> Response { + var generator = SeedRandomNumberGenerator(seed: seed ?? Int.random(in: Int.min...Int.max)) + let weatherCondition = weatherCondition ?? WeatherCondition.allCases.randomElement(using: &generator)! + let maxTemperature = maxTemperature ?? Int.random(in: 10...40, using: &generator) + let minTemperature = minTemperature ?? Int.random(in: -40.. Date: Wed, 1 May 2024 22:38:48 +0900 Subject: [PATCH 15/24] Revert "edit: rename callbackFetchWeatherList to fetchWeather" This reverts commit 8ba2766b83aea8e21509ae02e8b55b81be7c5751. --- Sources/YumemiWeather/YumemiWeather.swift | 4 ++-- Sources/YumemiWeather/YumemiWeatherList.swift | 4 ++-- Tests/YumemiWeatherTests/YumemiWeatherTests.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 18b97d1..bcd91ff 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -129,7 +129,7 @@ final public class YumemiWeather { /// - Parameters: /// - jsonString: 地域と日付を含む JSON 文字列 /// - completion: 完了コールバック - public static func fetchWeather(_ jsonString: String, completion: @escaping (Result) -> Void) { + public static func callbackFetchWeather(_ jsonString: String, completion: @escaping (Result) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + apiDuration) { do { let response = try fetchWeather(jsonString) @@ -168,7 +168,7 @@ final public class YumemiWeather { @available(iOS 13, macOS 10.15, *) public static func asyncFetchWeather(_ jsonString: String) async throws -> String { return try await withCheckedThrowingContinuation { continuation in - fetchWeather(jsonString) { result in + callbackFetchWeather(jsonString) { result in continuation.resume(with: result) } } diff --git a/Sources/YumemiWeather/YumemiWeatherList.swift b/Sources/YumemiWeather/YumemiWeatherList.swift index 9ff9902..c54a5f1 100644 --- a/Sources/YumemiWeather/YumemiWeatherList.swift +++ b/Sources/YumemiWeather/YumemiWeatherList.swift @@ -136,7 +136,7 @@ public extension YumemiWeather { /// - Parameters: /// - jsonString: 地域と日付を含む JSON 文字列 /// - completion: 完了コールバック - static func fetchWeatherList(_ jsonString: String, completion: @escaping (Result) -> Void) { + static func callbackFetchWeatherList(_ jsonString: String, completion: @escaping (Result) -> Void) { DispatchQueue.global().asyncAfter(deadline: .now() + apiDuration) { do { let response = try fetchWeatherList(jsonString) @@ -178,7 +178,7 @@ public extension YumemiWeather { @available(iOS 13, macOS 10.15, *) static func asyncFetchWeatherList(_ jsonString: String) async throws -> String { return try await withCheckedThrowingContinuation { continuation in - fetchWeatherList(jsonString) { result in + callbackFetchWeatherList(jsonString) { result in continuation.resume(with: result) } } diff --git a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift index 322a95c..92468f9 100644 --- a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift +++ b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift @@ -80,7 +80,7 @@ final class YumemiWeatherTests: XCTestCase { } """ let exp = expectation(description: #function) - YumemiWeather.fetchWeather(parameter) { result in + YumemiWeather.callbackFetchWeather(parameter) { result in exp.fulfill() switch result { case .success(let jsonString): From 6965ed4f3571be999acf5da8c2ede7f7118fffa4 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Thu, 2 May 2024 13:40:20 +0900 Subject: [PATCH 16/24] =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E5=90=8D=E5=89=8D=E7=A9=BA=E9=96=93=E3=81=AE=E6=8C=87=E5=AE=9A?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index bcd91ff..0f462d5 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -18,7 +18,7 @@ enum WeatherCondition: String, CaseIterable { case rainy } -public enum YumemiWeatherError: Swift.Error { +public enum YumemiWeatherError: Error { case invalidParameterError case unknownError } From ddef52828b6fef1d6dcef0f4a868a1299878bf1a Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Thu, 2 May 2024 15:55:14 +0900 Subject: [PATCH 17/24] =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=82=B3=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=81=95=E3=82=8C=E3=81=9F=E3=82=82=E3=81=AE=E3=81=8C?= =?UTF-8?q?=E9=81=A9=E5=88=87=E3=81=AB=E3=83=87=E3=82=B3=E3=83=BC=E3=83=89?= =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 0f462d5..8d662de 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -187,6 +187,7 @@ extension YumemiWeather { static let decoder: JSONDecoder = { let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase decoder.dateDecodingStrategy = .formatted(dateFormatter) return decoder }() From 691177484ca71cacd380e7735bd1e535a7a9be9b Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Thu, 2 May 2024 16:46:38 +0900 Subject: [PATCH 18/24] =?UTF-8?q?=E4=B9=B1=E6=95=B0=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E3=81=8C=E7=8B=AC=E8=87=AA=E5=AE=9A=E7=BE=A9=E3=81=95?= =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B=E3=81=93=E3=81=A8=E3=81=8C?= =?UTF-8?q?=E6=83=B3=E5=83=8F=E3=81=97=E3=82=84=E3=81=99=E3=81=84=E5=90=8D?= =?UTF-8?q?=E5=89=8D=20ControllableGenerator=20=E3=81=AB=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=81=9D=E3=81=AE=E6=B8=A1=E3=81=97=E6=96=B9?= =?UTF-8?q?=E3=82=92=E6=A8=99=E6=BA=96=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9?= =?UTF-8?q?=E3=83=AA=E3=83=BC=E3=81=AB=E6=B2=BF=E3=81=A3=E3=81=9F=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E3=81=AB=E5=A4=89=E6=9B=B4=E3=81=97=E3=81=BE=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 意図した乱数を作り出すために makeRandomResponse に seed を渡す設計になっていましたが、現在の標準ライブラリーでは some RandomNumberGenerator を渡すのが一般的のため、それに合わせました。 また、seed によって乱数が制御されることは ControllableGenerator 器固有の仕様なため、makeRandomResponse には渡さず、それを ControllableGenerator 自身が制御するようにしました。この際、内部で使われる srand48 関数はグローバルに影響するため、イニシャライザーで呼び出すとほかのインスタンスにも影響します。その性質を考慮して、ControllableGenerator はシングルトンで実装するようにしました。 加えて、seed のリセットも ControllableGenerator の範疇と思われるため、必要な値を受け取って適切にシードをリセットする機能もここに移動しました。 その他として、シングルトンを static var で保持させています。これは乱数生成器を各種 API が inout で受け取る仕様になっているため、そこにそのまま渡せるようにするための措置です。自由に書き換え可能にはなりますが、そもそもインスタンスが1つしか存在せず、そのイニシャライザーはプライベートで保護されているため、予期しないインスタンスの入れ替え操作は起こりません。 --- .../YumemiWeather/ControllableGenerator.swift | 56 +++++++++++++++++++ .../SeedRandomNumberGenerator.swift | 19 ------- Sources/YumemiWeather/YumemiWeather.swift | 23 ++++++-- Sources/YumemiWeather/YumemiWeatherList.swift | 10 ++-- .../YumemiWeatherListTests.swift | 21 +++++++ .../YumemiWeatherTests.swift | 41 ++++++++++++++ 6 files changed, 140 insertions(+), 30 deletions(-) create mode 100644 Sources/YumemiWeather/ControllableGenerator.swift delete mode 100644 Sources/YumemiWeather/SeedRandomNumberGenerator.swift diff --git a/Sources/YumemiWeather/ControllableGenerator.swift b/Sources/YumemiWeather/ControllableGenerator.swift new file mode 100644 index 0000000..d7f5d08 --- /dev/null +++ b/Sources/YumemiWeather/ControllableGenerator.swift @@ -0,0 +1,56 @@ +// +// ControllableGenerator.swift +// +// +// Created by 古宮 伸久 on 2022/04/08. +// + +import Foundation + +/// 制御可能な乱数生成器です。 +struct ControllableGenerator { + + /// 唯一のインスタンスを生成します。 + /// + /// このイニシャライザーは、複数回呼び出しても意味がありません。 + /// 乱数生成方法の都合で、同じ振る舞いをするインスタンスになります。 + private init() { + } +} + +extension ControllableGenerator { + + /// 唯一の、制御可能な乱数生成器インスタンスです。 + static var shared = ControllableGenerator() + + /// 乱数生成時に使用するシード値を `seed` でリセットします。 + /// - Parameter seed: 新たに使用するシード値です。 + static func reset(withSeed seed: Int) { + srand48(seed) + } + + /// 乱数生成時に使用するシード値を `area` と `date` から算出してリセットします。 + /// - Parameters: + /// - area: シード値の算出に使う、地域情報 + /// - date: シード値の算出に使う、日付情報 + static func resetUsing(area: Area, date: Date) { + + var hasher = Hasher() + + hasher.combine(area) + hasher.combine(date) + + let seed = hasher.finalize() + + reset(withSeed: seed) + } +} + +extension ControllableGenerator : RandomNumberGenerator { + + /// 次の乱数を生成します。 + /// - Returns: 次の乱数です。 + func next() -> UInt64 { + UInt64(drand48() * Double(UInt64.max)) + } +} diff --git a/Sources/YumemiWeather/SeedRandomNumberGenerator.swift b/Sources/YumemiWeather/SeedRandomNumberGenerator.swift deleted file mode 100644 index 1c3c5da..0000000 --- a/Sources/YumemiWeather/SeedRandomNumberGenerator.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// SeedRandomNumberGenerator.swift -// -// -// Created by 古宮 伸久 on 2022/04/08. -// - -import Foundation - -struct SeedRandomNumberGenerator: RandomNumberGenerator { - init(seed: Int) { - // Set the random seed - srand48(seed) - } - - func next() -> UInt64 { - UInt64(drand48() * Double(UInt64.max)) - } -} diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 8d662de..f69b3f8 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -205,17 +205,30 @@ extension YumemiWeather { /// - maxTemperature: 最高気温 /// - minTemperature: 最低気温 /// - date: 日付 - /// - seed: シード値 /// - Returns: Response構造体 - static func makeRandomResponse( weatherCondition: WeatherCondition? = nil, maxTemperature: Int? = nil, minTemperature: Int? = nil, - date: Date? = nil, - seed: Int? = nil + date: Date? = nil + ) -> Response { + return makeRandomResponse(using: &ControllableGenerator.shared, weatherCondition: weatherCondition, maxTemperature: maxTemperature, minTemperature: minTemperature) + } + + /// 引数の値でResponse構造体を作成する。引数がnilの場合はランダムに値を作成する。 + /// - Parameters: + /// - weatherCondition: 天気状況を表すenum + /// - maxTemperature: 最高気温 + /// - minTemperature: 最低気温 + /// - date: 日付 + /// - Returns: Response構造体 + static func makeRandomResponse( + using generator: inout some RandomNumberGenerator, + weatherCondition: WeatherCondition? = nil, + maxTemperature: Int? = nil, + minTemperature: Int? = nil, + date: Date? = nil ) -> Response { - var generator = SeedRandomNumberGenerator(seed: seed ?? Int.random(in: Int.min...Int.max)) let weatherCondition = weatherCondition ?? WeatherCondition.allCases.randomElement(using: &generator)! let maxTemperature = maxTemperature ?? Int.random(in: 10...40, using: &generator) let minTemperature = minTemperature ?? Int.random(in: -40.. AreaResponse in - var hasher = Hasher() - hasher.combine(area) - hasher.combine(request.date) - return AreaResponse(area: area, info: makeRandomResponse(date: request.date, seed: hasher.finalize())) + let areaResponses = areas.map { area -> AreaResponse in + ControllableGenerator.resetUsing(area: area, date: request.date) + return AreaResponse(area: area, info: makeRandomResponse(using: &ControllableGenerator.shared, date: request.date)) } - let responseData = try encoder.encode(response) + let responseData = try encoder.encode(areaResponses) return String(data: responseData, encoding: .utf8)! } diff --git a/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift b/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift index eb1ee51..2b75646 100644 --- a/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift +++ b/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift @@ -3,6 +3,27 @@ import XCTest final class YumemiWeatherListTests: XCTestCase { + func test_Areasに空を指定したときに全ての地域が取得される() throws { + + let request = """ + { + "areas": [], + "date": "2020-04-01T12:00:00+09:00" + } + """ + + let responseJSON = try YumemiWeather.fetchWeatherList(request) + + guard let responseData = responseJSON.data(using: .utf8) else { + XCTFail("Illegal response data: \(responseJSON)") + return + } + + let response = try YumemiWeather.decoder.decode([AreaResponse].self, from: responseData) + + XCTAssertEqual(response.map(\.area), Area.allCases) + } + func test_fetchWeatherList_jsonString() { let parameter = """ { diff --git a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift index 92468f9..eab6d6f 100644 --- a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift +++ b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift @@ -3,6 +3,47 @@ import XCTest final class YumemiWeatherTests: XCTestCase { + func test_ランダムにレスポンスを生成する() { + + // 日付を省略すると実行した瞬間のものが得られてしまうため、 + // テストするために固定とします。 + let date = Date() + + func makeRandomResponse(withSeed seed: Int) -> Response { + + ControllableGenerator.reset(withSeed: seed) + return YumemiWeather.makeRandomResponse(using: &ControllableGenerator.shared, date: date) + } + + let response1 = makeRandomResponse(withSeed: 0) + let response2 = makeRandomResponse(withSeed: 1546000) + let response3 = makeRandomResponse(withSeed: 0) + let response4 = makeRandomResponse(withSeed: 1546000) + + XCTAssertEqual(response1, response3) + XCTAssertEqual(response2, response4) + + XCTAssertEqual(response1.weatherCondition, "sunny") + XCTAssertEqual(response1.minTemperature, -33) + XCTAssertEqual(response1.maxTemperature, 33) + XCTAssertEqual(response1.date, date) + + XCTAssertEqual(response2.weatherCondition, "cloudy") + XCTAssertEqual(response2.minTemperature, 23) + XCTAssertEqual(response2.maxTemperature, 28) + XCTAssertEqual(response2.date, date) + + XCTAssertEqual(response3.weatherCondition, "sunny") + XCTAssertEqual(response3.minTemperature, -33) + XCTAssertEqual(response3.maxTemperature, 33) + XCTAssertEqual(response3.date, date) + + XCTAssertEqual(response4.weatherCondition, "cloudy") + XCTAssertEqual(response4.minTemperature, 23) + XCTAssertEqual(response4.maxTemperature, 28) + XCTAssertEqual(response4.date, date) + } + func test_fetchWeather() { let str = YumemiWeather.fetchWeatherCondition() XCTAssertNotNil(WeatherCondition(rawValue: str)) From 121b753658d1b8c80e1b809b8811edcae03c4d83 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Thu, 2 May 2024 16:55:52 +0900 Subject: [PATCH 19/24] =?UTF-8?q?=E6=A8=99=E6=BA=96=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=83=96=E3=83=A9=E3=83=AA=E3=83=BC=E3=81=AE=E4=BD=9C=E6=B3=95?= =?UTF-8?q?=E3=81=AB=E5=9F=BA=E3=81=A5=E3=81=84=E3=81=A6=E3=80=81WeatherCo?= =?UTF-8?q?ndition=20=E3=82=92=E3=83=A9=E3=83=B3=E3=83=80=E3=83=A0?= =?UTF-8?q?=E3=81=A7=E7=94=9F=E6=88=90=E3=81=99=E3=82=8B=E3=82=B3=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=82=92=E5=A4=89=E6=9B=B4=E3=81=97=E3=81=BE=E3=81=97?= =?UTF-8?q?=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index f69b3f8..db43983 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -18,6 +18,22 @@ enum WeatherCondition: String, CaseIterable { case rainy } +extension WeatherCondition { + + /// 天候をランダムで取得します。 + /// - Returns: なにかしらの転向を返します。 + static func random() -> Self { + random(using: &ControllableGenerator.shared) + } + + /// 天候をランダムで取得します。 + /// - Parameter generator: ランダムで取得するのに使う乱数生成期です。 + /// - Returns: なにかしらの転向を返します。 + static func random(using generator: inout some RandomNumberGenerator) -> Self { + WeatherCondition.allCases.randomElement(using: &generator)! + } +} + public enum YumemiWeatherError: Error { case invalidParameterError case unknownError @@ -229,7 +245,7 @@ extension YumemiWeather { minTemperature: Int? = nil, date: Date? = nil ) -> Response { - let weatherCondition = weatherCondition ?? WeatherCondition.allCases.randomElement(using: &generator)! + let weatherCondition = weatherCondition ?? WeatherCondition.random(using: &generator) let maxTemperature = maxTemperature ?? Int.random(in: 10...40, using: &generator) let minTemperature = minTemperature ?? Int.random(in: -40.. Date: Thu, 2 May 2024 17:38:16 +0900 Subject: [PATCH 20/24] =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E5=90=8D=E5=89=8D=E7=A9=BA=E9=96=93=E6=8C=87=E5=AE=9A=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index db43983..b9a0f81 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -30,7 +30,7 @@ extension WeatherCondition { /// - Parameter generator: ランダムで取得するのに使う乱数生成期です。 /// - Returns: なにかしらの転向を返します。 static func random(using generator: inout some RandomNumberGenerator) -> Self { - WeatherCondition.allCases.randomElement(using: &generator)! + allCases.randomElement(using: &generator)! } } From 718cc61d1483bdf97e0f53d00bdbe71debcdeec3 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Fri, 3 May 2024 00:20:29 +0900 Subject: [PATCH 21/24] =?UTF-8?q?=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=81=AE?= =?UTF-8?q?=E8=AA=A4=E5=AD=97=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97=E3=81=BE?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index b9a0f81..0b312ca 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -21,14 +21,14 @@ enum WeatherCondition: String, CaseIterable { extension WeatherCondition { /// 天候をランダムで取得します。 - /// - Returns: なにかしらの転向を返します。 + /// - Returns: なにかしらの天候を返します。 static func random() -> Self { random(using: &ControllableGenerator.shared) } /// 天候をランダムで取得します。 /// - Parameter generator: ランダムで取得するのに使う乱数生成期です。 - /// - Returns: なにかしらの転向を返します。 + /// - Returns: なにかしらの天候を返します。 static func random(using generator: inout some RandomNumberGenerator) -> Self { allCases.randomElement(using: &generator)! } From feee4401e34b2edad455ef49e932ee9e4aa8b6f5 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Fri, 3 May 2024 00:32:41 +0900 Subject: [PATCH 22/24] =?UTF-8?q?=E4=B8=8D=E5=BF=85=E8=A6=81=E3=81=AA?= =?UTF-8?q?=E5=90=8D=E5=89=8D=E7=A9=BA=E9=96=93=E3=81=AE=E6=98=8E=E8=A8=98?= =?UTF-8?q?=E3=82=92=E7=9C=81=E7=95=A5=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 0b312ca..aae67cb 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -245,9 +245,9 @@ extension YumemiWeather { minTemperature: Int? = nil, date: Date? = nil ) -> Response { - let weatherCondition = weatherCondition ?? WeatherCondition.random(using: &generator) - let maxTemperature = maxTemperature ?? Int.random(in: 10...40, using: &generator) - let minTemperature = minTemperature ?? Int.random(in: -40.. Date: Thu, 9 May 2024 22:22:29 +0900 Subject: [PATCH 23/24] =?UTF-8?q?YumemiWeather=20=E3=81=AE=20List=20?= =?UTF-8?q?=E5=8F=96=E5=BE=97=E7=94=A8=20API=20=E3=81=A7=E3=80=81=E5=9C=B0?= =?UTF-8?q?=E5=9F=9F=E3=81=AB=E7=A9=BA=E3=82=92=E6=8C=87=E5=AE=9A=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=81=A8=E5=85=A8=E5=9C=B0=E5=9F=9F=E3=81=AE=E4=BA=88?= =?UTF-8?q?=E5=A0=B1=E3=81=8C=E5=8F=96=E5=BE=97=E3=81=A7=E3=81=8D=E3=82=8B?= =?UTF-8?q?=E6=97=A8=E3=82=92=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=81=AB=E8=A8=98?= =?UTF-8?q?=E8=BC=89=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82=E6=93=AC?= =?UTF-8?q?=E4=BC=BC=20API=20=E3=81=A7=E3=81=82=E3=82=8B=E3=81=93=E3=81=A8?= =?UTF-8?q?=E3=81=AE=E8=A8=98=E8=BF=B0=E3=82=92=E5=90=84=20API=20=E3=81=A7?= =?UTF-8?q?=E3=81=AF=E3=81=AA=E3=81=8F=20YumemiWeather=20=E5=9E=8B?= =?UTF-8?q?=E3=81=AE=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88=E3=81=AB=E8=A8=98=E8=BC=89?= =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82=E3=81=9D=E3=82=8C?= =?UTF-8?q?=E3=81=AB=E4=BC=B4=E3=81=84=E3=80=81=E5=8D=98=E4=BD=93=E5=8F=96?= =?UTF-8?q?=E5=BE=97=E7=94=A8=E3=81=AE=20API=20=E3=81=A7=E3=81=AE=E3=83=89?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=82=B3=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=82=E8=AA=BF=E6=95=B4=E3=81=97=E3=80=81?= =?UTF-8?q?=E5=85=A8=E4=BD=93=E3=81=AB=E7=B5=B1=E4=B8=80=E6=84=9F=E3=82=92?= =?UTF-8?q?=E6=8C=81=E3=81=9F=E3=81=9B=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/YumemiWeather/YumemiWeather.swift | 29 +++++++++++++++---- Sources/YumemiWeather/YumemiWeatherList.swift | 29 ++++++++++++++++--- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index aae67cb..6e69d86 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -39,15 +39,20 @@ public enum YumemiWeatherError: Error { case unknownError } +/// 天気予報を取得する擬似天気予報 API です。 +/// +/// ゆめみ iOS 研修用の実装であり、実際の天気予報ではなくランダムな天気予報が得られます。 +/// 研修効果を高めるため、さまざまなバージョンの API が用意されていて、 +/// その中には適切なパラメーターを与えたときでも一定の確率でエラーを返すものもあります。 final public class YumemiWeather { - /// 擬似 天気予報 API Simple ver + /// 天気予報を読み込む API の Simple Version です。 /// - Returns: 天気状況を表す文字列 "sunny" or "cloudy" or "rainy" public static func fetchWeatherCondition() -> String { return makeRandomResponse().weatherCondition } - /// 擬似 天気予報 API Throws ver + /// 天気予報を読み込む API の Throwing Version です。 /// - Throws: YumemiWeatherError /// - Parameters: /// - area: 天気予報を取得する対象地域 example: "tokyo" @@ -60,7 +65,10 @@ final public class YumemiWeather { return makeRandomResponse().weatherCondition } - /// 擬似 天気予報 API JSON ver + /// 天気予報を読み込む API の JSON Version です。 + /// + /// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// 取得された天気予報は速やかに返されます。 /// /// API に請求する JSON 文字列の例: /// @@ -97,7 +105,10 @@ final public class YumemiWeather { return String(data: responseData, encoding: .utf8)! } - /// 擬似 天気予報 API Sync ver + /// 天気予報を読み込む API の Sync Version です。 + /// + /// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// この API は同期的に実行され、天気予報を返すまでに若干時間がかかります。 /// /// API に請求する JSON 文字列の例: /// @@ -123,7 +134,10 @@ final public class YumemiWeather { return try fetchWeather(jsonString) } - /// 擬似 天気予報 API Callback ver + /// 天気予報を読み込む API の Callback Version です。 + /// + /// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// この API は非同期的に実行され、天気予報を取得できるとその結果を添えて `completion` を呼び出します。 /// /// API に請求する JSON 文字列の例: /// @@ -160,8 +174,11 @@ final public class YumemiWeather { } } - /// 擬似 天気予報 API Async ver + /// 天気予報を読み込む API の Async Version です。 /// + /// JSON 文字列で地域情報 `area` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// この API は非同期的に実行され、天気予報を取得できるまでは Swift Concurrency により処理が中断されます。 + /// /// API に請求する JSON 文字列の例: /// /// { diff --git a/Sources/YumemiWeather/YumemiWeatherList.swift b/Sources/YumemiWeather/YumemiWeatherList.swift index 0a7c29d..45fd4a9 100644 --- a/Sources/YumemiWeather/YumemiWeatherList.swift +++ b/Sources/YumemiWeather/YumemiWeatherList.swift @@ -34,7 +34,12 @@ public enum Area: String, CaseIterable, Codable { public extension YumemiWeather { - /// 擬似 天気予報一覧 API JSON ver + /// 天気予報一覧を読み込む API の JSON Version です。 + /// + /// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// 取得された天気予報は速やかに返されます。 + /// + /// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。 /// /// API に請求する JSON 文字列の例: /// @@ -42,6 +47,7 @@ public extension YumemiWeather { /// "areas": ["Tokyo"], /// "date": "2020-04-01T12:00:00+09:00" /// } + /// /// 返された AreaResponse の JSON 文字列の例 /// /// [ @@ -78,7 +84,12 @@ public extension YumemiWeather { return String(data: responseData, encoding: .utf8)! } - /// 擬似 天気予報一覧 API Sync ver + /// 天気予報一覧を読み込む API の Sync Version です。 + /// + /// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// この API は同期的に実行され、天気予報を返すまでに若干時間がかかります。 + /// + /// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。 /// /// API に請求する JSON 文字列の例: /// @@ -107,7 +118,12 @@ public extension YumemiWeather { return try fetchWeatherList(jsonString) } - /// 擬似 天気予報一覧 API Callback ver + /// 天気予報一覧を読み込む API の Callback Version です。 + /// + /// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// この API は非同期的に実行され、天気予報を取得できるとその結果を添えて `completion` を呼び出します。 + /// + /// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。 /// /// API に請求する JSON 文字列の例: /// @@ -149,7 +165,12 @@ public extension YumemiWeather { } } - /// 擬似 天気予報一覧API Async ver + /// 天気予報一覧を読み込む API の Async Version です。 + /// + /// JSON 文字列で地域情報 `areas` と日付情報 `date` を持つオブジェクトを受け取って、それに該当する天気予報を取得します。 + /// この API は非同期的に実行され、天気予報を取得できるまでは Swift Concurrency により処理が中断されます。 + /// + /// 地域情報は配列で複数の地域を一括指定し、それらの地域の天気予報を取得できます。地域を指定しない場合は全地域の天気予報を取得します。 /// /// API に請求する JSON 文字列の例: /// From 9cfb85bcd4faa31d99ef6c0f430c634590ce49c2 Mon Sep 17 00:00:00 2001 From: Tomohiro Kumagai Date: Thu, 2 May 2024 22:43:16 +0900 Subject: [PATCH 24/24] =?UTF-8?q?YumemiWeather=20API=20=E3=81=8C=E7=84=A1?= =?UTF-8?q?=E4=BD=9C=E7=82=BA=E3=81=AB=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92?= =?UTF-8?q?=E8=BF=94=E3=81=99=E9=A0=BB=E5=BA=A6=E3=82=92=E3=82=B3=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=83=AD=E3=83=BC=E3=83=AB=E5=8F=AF=E8=83=BD=E3=81=AB?= =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=97=E3=81=9F=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../YumemiWeather.APIQuality.swift | 54 +++++++ Sources/YumemiWeather/YumemiWeather.swift | 12 +- Sources/YumemiWeather/YumemiWeatherList.swift | 4 +- Tests/YumemiWeatherTests/TestMethods.swift | 67 ++++++++ .../YumemiWeatherListTests.swift | 150 ++++++++---------- .../YumemiWeatherTests.swift | 90 ++++------- 6 files changed, 226 insertions(+), 151 deletions(-) create mode 100644 Sources/YumemiWeather/YumemiWeather.APIQuality.swift create mode 100644 Tests/YumemiWeatherTests/TestMethods.swift diff --git a/Sources/YumemiWeather/YumemiWeather.APIQuality.swift b/Sources/YumemiWeather/YumemiWeather.APIQuality.swift new file mode 100644 index 0000000..2a059f3 --- /dev/null +++ b/Sources/YumemiWeather/YumemiWeather.APIQuality.swift @@ -0,0 +1,54 @@ +// +// YumemiWeather.APIQuality.swift +// +// +// Created by Tomohiro Kumagai on 2024/05/02 +// +// + +extension YumemiWeather { + + /// API の品質を表現します。 + enum APIQuality { + case sometimesFails(probability: Double) + case alwaysFails + case neverFails + } +} + +extension YumemiWeather { + + /// API の想定品質です。 + static var apiQuality: APIQuality = .sometimesFails(probability: 0.25) + + static func whetherHit(with probability: Double) -> Bool { + return (0 ..< probability).contains(.random(in: 0 ..< 1)) + } + + /// この場所に不安定要素を埋め込みます。 + /// + /// 不安定さの度合いは `apiQuality` に依存します。 + /// - Throws: YumemiError ここで失敗が生成されると YumemiWeatherError.unknownError が送出されます。 + static func introduceInstability() throws { + + switch apiQuality { + + case .neverFails: + return + + case .sometimesFails(let probability) where !whetherHit(with: probability): + return + + case .sometimesFails, .alwaysFails: + throw YumemiWeatherError.unknownError + } + } + + /// この場所に不安定要素を埋め込みます。 + /// + /// 不安定さの度合いは `apiQuality` に依存します。 + /// - Throws: YumemiError ここで失敗が生成されると YumemiWeatherError.unknownError が送出されます。 + func introduceInstability() throws { + try Self.introduceInstability() + } +} diff --git a/Sources/YumemiWeather/YumemiWeather.swift b/Sources/YumemiWeather/YumemiWeather.swift index 6e69d86..ca3bd46 100644 --- a/Sources/YumemiWeather/YumemiWeather.swift +++ b/Sources/YumemiWeather/YumemiWeather.swift @@ -58,11 +58,8 @@ final public class YumemiWeather { /// - area: 天気予報を取得する対象地域 example: "tokyo" /// - Returns: 天気状況を表す文字列 "sunny" or "cloudy" or "rainy" public static func fetchWeatherCondition(at area: String) throws -> String { - if Int.random(in: 0...4) == 4 { - throw YumemiWeatherError.unknownError - } - - return makeRandomResponse().weatherCondition + try introduceInstability() + return self.makeRandomResponse().weatherCondition } /// 天気予報を読み込む API の JSON Version です。 @@ -98,10 +95,7 @@ final public class YumemiWeather { let response = makeRandomResponse(date: request.date) let responseData = try encoder.encode(response) - if Int.random(in: 0...4) == 4 { - throw YumemiWeatherError.unknownError - } - + try introduceInstability() return String(data: responseData, encoding: .utf8)! } diff --git a/Sources/YumemiWeather/YumemiWeatherList.swift b/Sources/YumemiWeather/YumemiWeatherList.swift index 45fd4a9..7f0ff13 100644 --- a/Sources/YumemiWeather/YumemiWeatherList.swift +++ b/Sources/YumemiWeather/YumemiWeatherList.swift @@ -70,9 +70,7 @@ public extension YumemiWeather { throw YumemiWeatherError.invalidParameterError } - if Int.random(in: 0...4) == 4 { - throw YumemiWeatherError.unknownError - } + try introduceInstability() let areas = request.areas.isEmpty ? Area.allCases : request.areas.compactMap { Area(rawValue: $0) } let areaResponses = areas.map { area -> AreaResponse in diff --git a/Tests/YumemiWeatherTests/TestMethods.swift b/Tests/YumemiWeatherTests/TestMethods.swift new file mode 100644 index 0000000..005e8bf --- /dev/null +++ b/Tests/YumemiWeatherTests/TestMethods.swift @@ -0,0 +1,67 @@ +// +// TestMethods.swift +// +// +// Created by Tomohiro Kumagai on 2024/05/02 +// +// + +import XCTest +@testable import YumemiWeather + +/// 失敗数が想定内かを判定します。 +/// - Parameters: +/// - probability: 失敗率です。 +/// - tryingCount: 総試行回数です。 +/// - failureCount: うち、失敗した回数です。 +/// - file: 判定コードが記載されたファイルです。 +/// - line: 判定コードが記載された行です。 +func XCTAssertFailureCount(probability: Double, tryingCount: Int, failureCount: Int, file: StaticString = #filePath, line: UInt = #line) { + + let estimatedFailureCount = Double(tryingCount) * probability + let expectedFailureMargin = Double(tryingCount) * 0.01 + + let expectedFailureRange = estimatedFailureCount - expectedFailureMargin ..< estimatedFailureCount + expectedFailureMargin + + XCTAssertTrue(expectedFailureRange.contains(Double(failureCount)), "Failures: \(failureCount), Estimated failures = \(estimatedFailureCount)±\(expectedFailureMargin)", file: file, line: line) +} + +/// YumemiWether API の品質をテストします。 +/// - Parameters: +/// - tryingCount: 総試行回数です。 +/// - quality: API の想定品質です。 +/// - file: 判定コードが記載されたファイルです。 +/// - line: 判定コードが記載された行です。 +func XCTAssertAPIQuality(_ predicate: () throws -> Void, tryingCount: Int, quality: YumemiWeather.APIQuality, file: StaticString = #filePath, line: UInt = #line) { + + var succeededCount = 0 + var failedCount = 0 + + YumemiWeather.apiQuality = quality + + for _ in 0 ..< tryingCount { + + switch Result(catching: predicate) { + + case .success(): + succeededCount += 1 + + case .failure(_): + failedCount += 1 + } + } + + switch quality { + + case .neverFails: + XCTAssertEqual(succeededCount, tryingCount, file: file, line: line) + XCTAssertEqual(failedCount, 0, file: file, line: line) + + case .alwaysFails: + XCTAssertEqual(succeededCount, 0, file: file, line: line) + XCTAssertEqual(failedCount, tryingCount, file: file, line: line) + + case .sometimesFails(let probability): + XCTAssertFailureCount(probability: probability, tryingCount: tryingCount, failureCount: failedCount, file: file, line: line) + } +} diff --git a/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift b/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift index 2b75646..2e50829 100644 --- a/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift +++ b/Tests/YumemiWeatherTests/YumemiWeatherListTests.swift @@ -3,6 +3,28 @@ import XCTest final class YumemiWeatherListTests: XCTestCase { + override func setUpWithError() throws { + YumemiWeather.apiQuality = .neverFails + } + + func test_APIは必ずしも成功しない() throws { + + let tryingCount = 15_000 + let request = """ + { + "area": "tokyo", + "date": "2020-04-01T12:00:00+09:00" + } + """ + let predicate = { _ = try YumemiWeather.fetchWeather(request) } + + XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .sometimesFails(probability: 0.25)) + XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .sometimesFails(probability: 0.8)) + XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .sometimesFails(probability: 0.05)) + XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .alwaysFails) + XCTAssertAPIQuality(predicate, tryingCount: tryingCount, quality: .neverFails) + } + func test_Areasに空を指定したときに全ての地域が取得される() throws { let request = """ @@ -24,115 +46,83 @@ final class YumemiWeatherListTests: XCTestCase { XCTAssertEqual(response.map(\.area), Area.allCases) } - func test_fetchWeatherList_jsonString() { + func test_fetchWeatherList_jsonString() throws { let parameter = """ { "areas": [], "date": "2020-04-01T12:00:00+09:00" } """ - do { - let responseJSON = try YumemiWeather.fetchWeatherList(parameter) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .formatted(dateFormatter) - let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) - XCTAssertEqual(response.count, Area.allCases.count) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + let responseJSON = try YumemiWeather.fetchWeatherList(parameter) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateFormatter) + let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) + XCTAssertEqual(response.count, Area.allCases.count) } - func test_fetchWeatherList_jsonString_one() { + func test_fetchWeatherList_jsonString_one() throws { let parameter = """ { "areas": ["Tokyo"], "date": "2020-04-01T12:00:00+09:00" } """ - do { - let responseJSON = try YumemiWeather.fetchWeatherList(parameter) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .formatted(dateFormatter) - let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) - XCTAssertEqual(response.count, 1) - let tokyo = response.first - XCTAssertEqual(tokyo?.area, .Tokyo) - - let responseJSON2 = try YumemiWeather.fetchWeatherList(parameter) - let response2 = try decoder.decode([AreaResponse].self, from: Data(responseJSON2.utf8)) - let tokyo2 = response2.first - XCTAssertEqual(tokyo?.info, tokyo2?.info) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + let responseJSON = try YumemiWeather.fetchWeatherList(parameter) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateFormatter) + let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) + XCTAssertEqual(response.count, 1) + let tokyo = response.first + XCTAssertEqual(tokyo?.area, .Tokyo) + + let responseJSON2 = try YumemiWeather.fetchWeatherList(parameter) + let response2 = try decoder.decode([AreaResponse].self, from: Data(responseJSON2.utf8)) + let tokyo2 = response2.first + XCTAssertEqual(tokyo?.info, tokyo2?.info) } - func test_fetchWeatherList_jsonString_two() { + func test_fetchWeatherList_jsonString_two() throws { let parameter = """ { "areas": ["Tokyo", "Nagoya"], "date": "2020-04-01T12:00:00+09:00" } """ - do { - let responseJSON = try YumemiWeather.fetchWeatherList(parameter) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .formatted(dateFormatter) - let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) - XCTAssertEqual(response.count, 2) - let tokyo = response.first(where: { $0.area == .Tokyo }) - XCTAssertNotNil(tokyo) - let nagoya = response.first(where: { $0.area == .Nagoya }) - XCTAssertNotNil(nagoya) - XCTAssertNotEqual(tokyo?.info, nagoya?.info) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + let responseJSON = try YumemiWeather.fetchWeatherList(parameter) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateFormatter) + let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) + XCTAssertEqual(response.count, 2) + let tokyo = response.first(where: { $0.area == .Tokyo }) + XCTAssertNotNil(tokyo) + let nagoya = response.first(where: { $0.area == .Nagoya }) + XCTAssertNotNil(nagoya) + XCTAssertNotEqual(tokyo?.info, nagoya?.info) } - func test_fetchWeatherList_jsonString_none() { + func test_fetchWeatherList_jsonString_none() throws { let parameter = """ { "areas": ["LosAngeles"], "date": "2020-04-01T12:00:00+09:00" } """ - do { - let responseJSON = try YumemiWeather.fetchWeatherList(parameter) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .formatted(dateFormatter) - let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) - XCTAssertEqual(response.count, 0) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + let responseJSON = try YumemiWeather.fetchWeatherList(parameter) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateFormatter) + let response = try decoder.decode([AreaResponse].self, from: Data(responseJSON.utf8)) + XCTAssertEqual(response.count, 0) } } diff --git a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift index eab6d6f..c15a480 100644 --- a/Tests/YumemiWeatherTests/YumemiWeatherTests.swift +++ b/Tests/YumemiWeatherTests/YumemiWeatherTests.swift @@ -3,6 +3,10 @@ import XCTest final class YumemiWeatherTests: XCTestCase { + override func setUpWithError() throws { + YumemiWeather.apiQuality = .neverFails + } + func test_ランダムにレスポンスを生成する() { // 日付を省略すると実行した瞬間のものが得られてしまうため、 @@ -49,44 +53,28 @@ final class YumemiWeatherTests: XCTestCase { XCTAssertNotNil(WeatherCondition(rawValue: str)) } - func test_fetchWeather_at() { - do { - let str = try YumemiWeather.fetchWeatherCondition(at: "tokyo") - XCTAssertNotNil(WeatherCondition(rawValue: str)) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + func test_fetchWeather_at() throws { + let str = try YumemiWeather.fetchWeatherCondition(at: "tokyo") + XCTAssertNotNil(WeatherCondition(rawValue: str)) } - func test_fetchWeather_jsonString() { + func test_fetchWeather_jsonString() throws { let parameter = """ { "area": "tokyo", "date": "2020-04-01T12:00:00+09:00" } """ - do { - let responseJSON = try YumemiWeather.fetchWeather(parameter) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .formatted(dateFormatter) - _ = try decoder.decode(Response.self, from: Data(responseJSON.utf8)) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + let responseJSON = try YumemiWeather.fetchWeather(parameter) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateFormatter) + _ = try decoder.decode(Response.self, from: Data(responseJSON.utf8)) } - func test_fetchWeather_jsonString_sync() { + func test_fetchWeather_jsonString_sync() throws { let beginDate = Date() let parameter = """ { @@ -94,21 +82,13 @@ final class YumemiWeatherTests: XCTestCase { "date": "2020-04-01T12:00:00+09:00" } """ - do { - let responseJSON = try YumemiWeather.syncFetchWeather(parameter) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .formatted(dateFormatter) - _ = try decoder.decode(Response.self, from: Data(responseJSON.utf8)) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + let responseJSON = try YumemiWeather.syncFetchWeather(parameter) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateFormatter) + _ = try decoder.decode(Response.self, from: Data(responseJSON.utf8)) XCTAssertGreaterThanOrEqual(Date().timeIntervalSince(beginDate), YumemiWeather.apiDuration) } @@ -139,7 +119,7 @@ final class YumemiWeatherTests: XCTestCase { } @available(iOS 13, macOS 10.15, *) - func test_fetchWeather_jsonString_async() async { + func test_fetchWeather_jsonString_async() async throws { let beginDate = Date() let parameter = """ { @@ -147,21 +127,13 @@ final class YumemiWeatherTests: XCTestCase { "date": "2020-04-01T12:00:00+09:00" } """ - do { - let responseJSON = try await YumemiWeather.asyncFetchWeather(parameter) - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .formatted(dateFormatter) - _ = try decoder.decode(Response.self, from: Data(responseJSON.utf8)) - } - catch let error as YumemiWeatherError { - XCTAssertEqual(error, YumemiWeatherError.unknownError) - } - catch { - XCTFail() - } + let responseJSON = try await YumemiWeather.asyncFetchWeather(parameter) + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .formatted(dateFormatter) + _ = try decoder.decode(Response.self, from: Data(responseJSON.utf8)) XCTAssertGreaterThanOrEqual(Date().timeIntervalSince(beginDate), YumemiWeather.apiDuration) }