質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

Q&A

解決済

1回答

1683閲覧

【swift】テストコード中のUndoManagerのundoメソッドが一回しか呼んでいなくてもregisterUndoを行った回数分呼ばれてしまう。

ichina

総合スコア6

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

Swift

Swiftは、アップルのiOSおよびOS Xのためのプログラミング言語で、Objective-CやObjective-C++と共存することが意図されています

0グッド

0クリップ

投稿2021/01/04 14:07

UndoManagerのundoメソッドを含むクラスのテストを行いたいのですが、例えばテストメソッド中でregisterUndoを行うターゲットのメソッドを3回コールするとundoをした際に一回のundoメソッドのコールでundoに登録した操作が3回実行されてしまいます。

UnitTestProjectTests

1import XCTest 2@testable import UnitTestProject 3 4class UnitTestProjectTests: XCTestCase { 5 6 func testUndo() { 7 let model = TestModel() 8 9 model.increment() 10 XCTAssertEqual(model.count, 1) 11 12 model.increment() 13 XCTAssertEqual(model.count, 2) 14 15 model.increment() 16 XCTAssertEqual(model.count, 3) 17 18 model.undo() 19 XCTAssertEqual(model.count, 2) 20 21 } 22}

TestModel

1import Foundation 2 3class TestModel { 4 5 let undoManager = UndoManager() 6 var count = 0 7 8 func registerUndo() { 9 if (undoManager.isUndoRegistrationEnabled) { 10 undoManager.registerUndo(withTarget: self, handler: { _ in 11 print("undo is executed") 12 self.count -= 1 13 }) 14 } 15 } 16 17 func increment() { 18 count += 1 19 registerUndo() 20 } 21 22 func undo() { 23 if(undoManager.canUndo){ 24 undoManager.undo() 25 } 26 } 27}

ターゲットクラスのregisterUndoではundo操作に分かりやすいようprintを入れています。
テストクラスではご覧の通りundoを一回しか呼んでいませんが、コンソールには"undo is executed"が3回表示され、実際にcountも0まで戻ってしまいます。

コンソールログ

Test Case '-[UnitTestProjectTests.UnitTestProjectTests testUndo]' started.
undo is executed
undo is executed
undo is executed
/Users/project/UnitTestProject/UnitTestProjectTests/UnitTestProjectTests.swift:27: error: -[UnitTestProjectTests.UnitTestProjectTests testUndo] : XCTAssertEqual failed: ("0") is not equal to ("2")
Test Case '-[UnitTestProjectTests.UnitTestProjectTests testUndo]' failed (0.019 seconds).
Test Suite 'UnitTestProjectTests' failed at 2021-01-04 22:21:43.585.
Executed 1 test, with 1 failure (0 unexpected) in 0.019 (0.020) seconds
Test Suite 'UnitTestProjectTests.xctest' failed at 2021-01-04 22:21:43.585.
Executed 1 test, with 1 failure (0 unexpected) in 0.019 (0.021) seconds
Test Suite 'Selected tests' failed at 2021-01-04 22:21:43.586.
Executed 1 test, with 1 failure (0 unexpected) in 0.019 (0.023) seconds

どうして1回しか呼んでいないのに3回実行されてしまうのでしょうか?
また、undo操作のテストはどのように行うべきでしょうか?

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

MasakiHori

2021/01/05 02:30

ユニットテストをきちんと行うのであればifに対するelseは必ず処理すべきと思います
ichina

2021/01/06 12:12

elseの場合は何も行わないようなコードでもログ出力などした方がいいということでしょうか?
MasakiHori

2021/01/07 02:03

すみません。 コードを読み間違えてました。 質問のコードでは特に必要がなかったですね。
guest

回答1

0

ベストアンサー

コメントレベルとなりますが、おそらく下記の議論とも関連してくるのだと思います。

この議論を参考に、ご提示いただいているコードを下記のように変更し、UndoManager に登録されるタイミングを調べてみます。

Swift

1import Foundation 2 3class TestModel { 4 5 let undoManager: UndoManager 6 var count = 0 7 8 init() { 9 self.undoManager = UndoManager() 10 11 // MARK: グループ化される時の通知を受け取る 12 registerForNotifications() 13 } 14 15 func registerUndo() { 16 if undoManager.isUndoRegistrationEnabled { 17 undoManager.registerUndo(withTarget: self, handler: { _ in 18 print("undo is executed") 19 self.count -= 1 20 }) 21 } 22 } 23 24 func increment() { 25 print("increment") 26 27 count += 1 28 registerUndo() 29 } 30 31 func undo() { 32 print("increment") 33 34 if undoManager.canUndo { 35 undoManager.undo() 36 } 37 } 38 39 // MARK: 以下の議論を参照 40 // https://stackoverflow.com/questions/47988403/how-will-undomanager-run-loop-grouping-be-affected-in-different-threading-contex 41 private func registerForNotifications() { 42 NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidOpenUndoGroup, object: undoManager, queue: nil) { _ in 43 print("opening group at level (self.undoManager.levelsOfUndo)") 44 } 45 46 NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidCloseUndoGroup, object: undoManager, queue: nil) { _ in 47 print("closing group at level (self.undoManager.levelsOfUndo)") 48 } 49 } 50}

これを、下記のようなテストケースで実行すると

Swift

1 func testUndo() { 2 let model = TestModel() 3 4 model.increment() 5 XCTAssertEqual(model.count, 1) 6 7 model.increment() 8 XCTAssertEqual(model.count, 2) 9 10 model.increment() 11 XCTAssertEqual(model.count, 3) 12 13 model.undo() 14 XCTAssertEqual(model.count, 2) 15 }

デバッグコンソールにはつぎのような表示が出てきます。

Console

1increment 2opening group at level 0 3increment 4increment 5increment 6closing group at level 0 7undo is executed 8undo is executed 9undo is executed

本来であれば、インクリメントのたびにグルーピングしてくれれば良いのですが、上記のログをみると3回分ののインクリメントをひとまとめにしてグルーピングされています。

なので、undo() を実行した時に3回分まとめて呼び出されているのだと推測されます(どこに記載されていたか見失ってしまいましたが、登録されるタイミングについては UndoManager のリファレンスのどこかに記載されていました)。

では、逐一登録するためにはどのようにすれば良いかというと、自動登録を解除し、毎回グルーピングさせることになるようです。

簡単に説明すると、groupsByEventfalse に設定し、登録したいタイミングで beginUndoGrouping()endUndoGrouping() を呼び出すことになります。

Swift

1class Counter { 2 private let manager: UndoManager 3 4 init() { 5 self.manager = UndoManager() 6 self.manager.groupsByEvent = false 7 8 registerForNotifications() 9 } 10 11 var count = 0 12 13 func redo() { 14 if manager.canRedo { 15 print("Redo") 16 manager.redo() 17 } else { 18 print("Cannot redo") 19 } 20 } 21 22 func undo() { 23 if manager.canUndo { 24 print("Undo") 25 manager.undo() 26 }else { 27 print("Cannot undo") 28 } 29 } 30 31 @objc func increment() { 32 print(#function) 33 34 count += 1 35 36 manager.beginUndoGrouping() 37 self.manager.registerUndo(withTarget: self, selector: #selector(decrement), object: nil) 38 manager.endUndoGrouping() 39 40 } 41 @objc func decrement() { 42 print(#function) 43 44 count -= 1 45 46 manager.beginUndoGrouping() 47 self.manager.registerUndo(withTarget: self, selector: #selector(increment), object: nil) 48 manager.endUndoGrouping() 49 } 50 51 func registerForNotifications() { 52 NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidOpenUndoGroup, object: manager, queue: nil) { _ in 53 print("opening group at level (self.manager.levelsOfUndo)") 54 } 55 56 NotificationCenter.default.addObserver(forName: Notification.Name.NSUndoManagerDidCloseUndoGroup, object: manager, queue: nil) { _ in 57 print("closing group at level (self.manager.levelsOfUndo)") 58 } 59 } 60} 61

ちなみに、上記のコードだと Undo だけでなく、Redo についても登録できるようになっています

公式マニュアル

  • [registerUndo(withTarget:selector:object:)

Registers the selector of the specified target to implement a single undo operation that the target receives.](https://developer.apple.com/documentation/foundation/undomanager/1414001-registerundo)

でも同じような例が紹介されています。

これを、下記のようなテストケースで実行させてみます。

Swift

1class UnitTestProjectTests: XCTestCase { 2 3 func testCounter() { 4 let counter = Counter() 5 6 counter.increment() 7 XCTAssertEqual(counter.count, 1) 8 9 counter.increment() 10 XCTAssertEqual(counter.count, 2) 11 12 counter.decrement() 13 XCTAssertEqual(counter.count, 1) 14 15 counter.undo() 16 XCTAssertEqual(counter.count, 2) 17 18 counter.undo() 19 XCTAssertEqual(counter.count, 1) 20 21 counter.undo() 22 XCTAssertEqual(counter.count, 0) 23 24 counter.redo() 25 XCTAssertEqual(counter.count, 1) 26 27 counter.redo() 28 XCTAssertEqual(counter.count, 2) 29 30 counter.increment() 31 XCTAssertEqual(counter.count, 3) 32 33 counter.redo() 34 XCTAssertEqual(counter.count, 3) 35 36 counter.undo() 37 XCTAssertEqual(counter.count, 2) 38 } 39}

こちらだと、テストは全て成功し、当然ですが動作も想定通りとなります。

Console

1increment() 2opening group at level 0 3closing group at level 0 4increment() 5opening group at level 0 6closing group at level 0 7decrement() 8opening group at level 0 9closing group at level 0 10Undo 11increment() 12Undo 13decrement() 14Undo 15decrement() 16Redo 17increment() 18Redo 19increment() 20increment() 21opening group at level 0 22closing group at level 0 23Cannot redo 24Undo 25decrement()

動作としては想定通りなのですが、なぜこのような処理を必要なのかという根本的な理由としては、私としても納得できない部分は残ってますし、それについては調べてみたのですが十分な確証は得られませんでした。

なので、あくまでも参考程度に見ていただければと思います。
冒頭の議論やこの結果などを参考にしていただくと、何か解決法がみつかるかもしれません。

投稿2021/01/05 02:18

TsukubaDepot

総合スコア5086

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

ichina

2021/01/06 12:14

毎回ありがとうございます。グルーピングの制御が入ってるとは思わず、公式をあまり読み込んでいませんでした。おかげさまでテスト成功しました。ありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問