Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

既存コードをAPI Guidelineに準拠させる #72

Merged
merged 25 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8ba2766
edit: rename callbackFetchWeatherList to fetchWeather
daikiumehara Apr 26, 2024
9520e85
edit: remove unnecessary makeResponse code
daikiumehara Apr 26, 2024
d385448
edit: refactor casting process
daikiumehara Apr 30, 2024
ebd0419
Example のテストコードで WeatherModel プロトコルの要求を満たせていなかったのを修正しました。
es-kumagai Apr 30, 2024
50276c9
Example テストコード内で、loadWeather メソッドにその呼び出し主が渡されていないのを修正しました。
es-kumagai Apr 30, 2024
70f228f
WeatherViewController に DisasterModelMock が設定されていないことが原因で、テストが強制終了するの…
es-kumagai Apr 30, 2024
2d2da35
WeatherImageView への反映を待たずに XCTAssertEqual で結果判定をして失敗していた箇所を修正しました。
es-kumagai Apr 30, 2024
f8d13ae
テストコード内の typo を修正しました。
es-kumagai Apr 30, 2024
81f35eb
テストコードでの View の準備は、ビューへのプロパティーアクセスを捨てる方法ではなく 'loadViewIfNeeded` を呼び出す…
es-kumagai Apr 30, 2024
c1fb6be
少なくとも現状のテストコードでは WeatherModelMock を 'setUpWithError' まで待たなくても準備できるため、…
es-kumagai Apr 30, 2024
acb3dda
単独で存在していた、不必要なドキュメントコメントを削除しました。
es-kumagai Apr 30, 2024
ee6cf6d
self. は省略される傾向があるため、それが明らかに不要な箇所については省略しました。
es-kumagai Apr 30, 2024
17daa5f
ThreadBlock の課題文に、使用する API の特徴説明を追記しました。
es-kumagai May 1, 2024
b7bf09c
YumemiWeather クラスの見通しを良くするために、外部公開する API の機能とそれ以外とを区別して定義するようにしました。
es-kumagai May 1, 2024
4a081da
Revert "edit: rename callbackFetchWeatherList to fetchWeather"
es-kumagai May 1, 2024
6965ed4
不必要な名前空間の指定を削除しました。
es-kumagai May 2, 2024
ddef528
エンコードされたものが適切にデコードできるように修正しました。
es-kumagai May 2, 2024
6911774
乱数生成器が独自定義されていることが想像しやすい名前 ControllableGenerator に変更し、その渡し方を標準ライブラリーに…
es-kumagai May 2, 2024
121b753
標準ライブラリーの作法に基づいて、WeatherCondition をランダムで生成するコードを変更しました。
es-kumagai May 2, 2024
3193e27
不必要な名前空間指定を削除しました。
es-kumagai May 2, 2024
718cc61
ドキュメントコメントの誤字を修正しました。
es-kumagai May 2, 2024
feee440
不必要な名前空間の明記を省略しました。
es-kumagai May 2, 2024
6d986e1
YumemiWeather の List 取得用 API で、地域に空を指定すると全地域の予報が取得できる旨をドキュメントコメントに記載し…
yumemi-kumagai May 9, 2024
9cfb85b
YumemiWeather API が無作為にエラーを返す頻度をコントロール可能にしました。
es-kumagai May 2, 2024
95aa9c9
Merge pull request #81 from yumemi-inc/feature/controllable_api_error
es-kumagai May 22, 2024
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
3 changes: 3 additions & 0 deletions Documentation/ThreadBlock.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Sync ver
```swift
static func syncFetchWeather(_ jsonString: String) throws -> String
```

このメソッドは、値を返すまでに少し時間がかかります。

[APIの概要](YumemiWeather.md)

## 課題
Expand Down
6 changes: 3 additions & 3 deletions Example/Example/Model/DisasterModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}
26 changes: 13 additions & 13 deletions Example/Example/UI/Weather/WeatherViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -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()
Expand All @@ -58,9 +58,9 @@ class WeatherViewController: UIViewController {
func handleWeather(result: Result<Response, WeatherError>) {
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
Expand All @@ -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)
}
}
}
Expand All @@ -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()
}
}
}
90 changes: 57 additions & 33 deletions Example/ExampleTests/WeatherViewControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,66 +12,90 @@ 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.view
weatherViewController = R.storyboard.weather.instantiateInitialViewController()!
weatherViewController.weatherModel = weatherModel
weatherViewController.disasterModel = DisasterModelMock()

weatherViewController.loadViewIfNeeded()
}

override func tearDownWithError() throws {
// 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 {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date())
@MainActor
func test_天気予報がsunnyだったらImageViewのImageにsunnyが設定されること_TintColorがredに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .sunny, maxTemp: 0, minTemp: 0, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.red())
XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.sunny())
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.red())
XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.sunny())
}

func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() throws {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date())
@MainActor
func test_天気予報がcloudyだったらImageViewのImageにcloudyが設定されること_TintColorがgrayに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .cloudy, maxTemp: 0, minTemp: 0, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.gray())
XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.cloudy())
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.gray())
XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.cloudy())
}

func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() throws {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date())
@MainActor
func test_天気予報がrainyだったらImageViewのImageにrainyが設定されること_TintColorがblueに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .rainy, maxTemp: 0, minTemp: 0, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.weatherImageView.tintColor, R.color.blue())
XCTAssertEqual(weahterViewController.weatherImageView.image, R.image.rainy())
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.weatherImageView.tintColor, R.color.blue())
XCTAssertEqual(weatherViewController.weatherImageView.image, R.image.rainy())
}

func test_最高気温_最低気温がUILabelに設定されること() throws {
weahterModel.fetchWeatherImpl = { _ in
Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date())
@MainActor
func test_最高気温_最低気温がUILabelに設定されること() async throws {
weatherModel.fetchWeatherImpl = { _ in
let response = Response(weather: .rainy, maxTemp: 100, minTemp: -100, date: Date())
return Result.success(response)
}

weahterViewController.loadWeather()
XCTAssertEqual(weahterViewController.minTempLabel.text, "-100")
XCTAssertEqual(weahterViewController.maxTempLabel.text, "100")
weatherViewController.loadWeather(self)
await Task.yield()

XCTAssertEqual(weatherViewController.minTempLabel.text, "-100")
XCTAssertEqual(weatherViewController.maxTempLabel.text, "100")
}
}

class WeatherModelMock: WeatherModel {

var fetchWeatherImpl: ((Request) throws -> Response)!
var fetchWeatherImpl: ((Request) -> Result<Response, WeatherError>)!

func fetchWeather(_ request: Request) throws -> Response {
return try fetchWeatherImpl(request)
func fetchWeather(at area: String, date: Date, completion: @escaping (Result<Response, WeatherError>) -> Void) {
completion(fetchWeatherImpl(Request(area: area, date: date)))
}
}

final class DisasterModelMock: DisasterModel {

func fetchDisaster(completion: ((String) -> Void)?) {
completion?("只今、災害情報はありません。")
}
}
56 changes: 56 additions & 0 deletions Sources/YumemiWeather/ControllableGenerator.swift
Original file line number Diff line number Diff line change
@@ -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))
}
}
19 changes: 0 additions & 19 deletions Sources/YumemiWeather/SeedRandomNumberGenerator.swift

This file was deleted.

2 changes: 1 addition & 1 deletion Sources/YumemiWeather/YumemiDisaster.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class YumemiDisaster {
public init() {}

public func fetchDisaster() {
self.delegate?.handle(disaster: "只今、災害情報はありません。")
delegate?.handle(disaster: "只今、災害情報はありません。")
}

}
54 changes: 54 additions & 0 deletions Sources/YumemiWeather/YumemiWeather.APIQuality.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading
Loading