Creating Tests

Now that we know how to write tests—create a method with “test” in its name, and use XCTAssert-type methods to say what should be true during the test—let’s put our existing functionality to the test.

We can start with the outlets that we created in the last chapter. Since the app launches and immediately loads our URL, we should see the last part of the URL as the titleLabel text, and the button should say “Play” or “Pause” based on the current player state.

Our tests will need to access the outlets we’re testing, and this presents a little bit of a hassle that we haven’t had to consider before. Swift considers all the classes in the PragmaticPodcasts target to be one module, and classes in a module can see each other’s properties and methods by default. However, PragmaticPodcastsTests is a different target and thus a different module, so it cannot see the methods or properties of our app’s classes. We’ll have to fix that before we can test anything.

We can declare different levels of access for our classes and their members. Swift has five levels of access, set by special keywords:

Access modifierVisibility

public

Visible everywhere, but cannot be overridden

open

Visible everywhere, and can be overridden

internal

Visible within the same module, and can be overridden

fileprivate

Visible only within the class itself, and extensions in the same source file

private

Visible only within the class itself, and not visible to extensions

The default level of access is internal, so the files in the PragmaticPodcasts module can see each other’s members, but PragmaticPodcastsTests can’t. We could declare the members we want to test (the titleLabel and playPauseButton) as public, but there’s a better way. The import directive can use a @testable keyword to open up internal members just for testing, and that’s what we need.

Notice that at the top of PragmaticPodcastsTests.swift, there’s already a @testable import set up for us:

 @testable​ ​import​ ​PragmaticPodcasts

This imports the PragmaticPodcasts module for use by the test class. By annotating it with the @testable keyword, we can access the properties and methods internal to the PragmaticPodcasts module, without having to make them public.

This will let our test code access the members of the ViewController class where we wrote all our functionality in the last chapter. Now let’s write a test to let us look inside that class.

Writing the Unit Tests

For our first test, let’s think of something easy. How about the title label that we wired up and coded in the last chapter? It seems like there should be a way to check that the string on that label is the last part of the URL that we’re playing.

Reading the label’s string will be easy. Actually, the trick will be how we get to the label. After all, look at the PragmaticPodcastsTests class—there’s no reference to anything in the app we’ve written. How can we even access the label to test it?

When we run a test, the simulator launches the whole app, so it’s possible for a test to ask the app itself about what it’s showing. But for a test, we would like to test a scene in isolation—if we wrote a test that assumes the player is the only scene in the app, then as soon as we start building out the app with lists of episodes, the test will break.

Loading Storyboard Scenes

Fortunately, we can load any scene we like directly from the storyboard. All we have to do is give the scene a name in the storyboard. Open Main.storyboard, go back to our one media-player scene, and select its View Controller (either from the orange ball on the top of the frame, or from the Document Outline pane on the left). Now bring up the Identity Inspector (3) in the right pane. There is a field here called Storyboard ID. This is where we can enter a name. Type in PlayerViewController. It doesn’t actually matter what name we use; we just have to remember to use this same name in our test class in the next step.

images/testing/ib-scene-storyboard-id.png

Now we can get this scene from within the test class, and from there, we’ll get individual components like the title label and the Play/Pause button.

We’re going to need to do this in any test we write, so getting the scene from the storyboard is something we should put in the setUp method, rather than in each of the tests. We’ll have setUp populate a property, and then read from that property in each test. So right before the empty setUp method, declare this property:

 var​ playerVC : ​ViewController​?

Recall that we’ve been writing our UI code so far in a class called ViewController, so we’re just creating a property of that type, called playerVC (to distinguish it from other view controllers we might test later). It has to be optional, because it can’t be populated by the time the test class’s init runs, since the setUp that will populate it is called after init.

Now we’re ready to implement setUp. We’re going to rely on the UIStoryboard class, which can load a storyboard by name, and any scene within that storyboard by identifier. So here’s how we’ll do that:

1: override​ ​func​ setUp() {
2: super​.setUp()
3: let​ storyboard = ​UIStoryboard​(name: ​"Main"​, bundle: ​nil​)
4: guard​ ​let​ playerVC = storyboard.instantiateViewController(withIdentifier:
5: "PlayerViewController"​) ​as?​ ​ViewController​ ​else​ { ​return​ }
6:  playerVC.loadViewIfNeeded()
7: self​.playerVC = playerVC
8: }

Line 3 is where we load the storyboard, by passing the name of Main.storyboard (minus its filename extension) to the UIStoryboard. On lines 4-5, we call the instantiateViewController method, passing in the PlayerViewController name that we gave the scene in the storyboard itself. This step is in a guard let, so if it fails—or if storyboard itself is actually nil because the previous step failed—we bail out without doing anything else.

On line 6, we know that playerVC is valid, but just having the view controller won’t actually load up any of the UI components until we actually access them. The app would do this for us when it’s running, of course, to put the components on the screen. Since our test won’t be doing that, we can load it manually with loadViewIfNeeded. Finally, on line 7, we assign the local playerVC to self.playerVC, the property that other methods in the class can see.

Using a Scene in a Test

Now that our scene is in the playerVC property, we can write test methods that use it. Let’s write one now. Delete the testExample and testPerformanceExample methods, since we don’t want them slowing us down doing nothing when we run tests.

Also, those test names aren’t very descriptive. One common pattern for naming tests is to indicate the thing being tested, the state under test, and the expected behavior. So, we could say, “For the player’s title label, once the URL is set, it should show the correct filename.”

As a test, this will be the method testPlayerTitleLabel_WhenURLSet_ShowsCorrectFilename. So let’s write this method. All it has to do is load the scene and then look at the string in the label.

1: func​ testPlayerTitleLabel_WhenURLSet_ShowsCorrectFilename() {
2: guard​ ​let​ playerVC = playerVC ​else​ {
3: XCTFail​(​"Couldn't load player scene"​)
4: return
5:  }
6: XCTAssertEqual​(​"CocoaConf001.m4a"​, playerVC.titleLabel.text)
7: }

Our method declaration on line 1 uses our very descriptive method-naming scheme, saying exactly what we’re testing, under what conditions, and what should happen. For the moment, the URL is set in viewDidLoad; later on, when we’re parsing podcast feeds, we’ll take that out and the test will need to be rewritten to set the URL. Of course, having tests that break when we make major changes like that is exactly what we want, right?

On lines 2-5, we use a guard let to collect the playerVC that we populated in setUp. If this didn’t work and playerVC is actually nil, then falling into the else should cause us to immediately fail the test, which we do with the XCTFail on line 3.

Finally, the moment of truth. On line 6, we can get the titleLabel from the playerVC and inspect the label’s text property. Is it the CocoaConf001.m4a filename from the URL like we expect? XCTAssertEqual will determine if we pass or fail.

Go ahead and run this test, either by clicking the diamond to the left of func testPlayerTitleLabel_WhenURLSet_ShowsCorrectFilename() (once Xcode figures out that this really is a test method and puts it there), or by clicking the test in the Test Navigator (5). The simulator will launch, you’ll see some activity in the Xcode status bar, and then you should see the “Test Succeeded” overlay that appears for a few seconds and fades out, along with the diamond next to the test method filling in with a green checkmark.

images/testing/xcode-individual-test-pass.png

Interacting with the System Under Test

One thing that makes our test really simple is the fact that it’s totally passive: all we have to do is read a value and assert that it’s the right thing. It’s actually more common to have to set things up a little.

Consider our Play/Pause button. We could write a test to make sure it says “Play” easily enough, but we also want to know that it says “Pause” once it’s playing. This will test that fancy KVO stuff we wrote in the last chapter.

But to get the player playing, we need to actually press the Play/Pause button or do something equivalent. Can a test do that? Well, think back to how we set up the connections: a tap on the button calls the method handlePlayPauseTapped. So we can just call that same method from a test, like this:

1: func​ testPlayerPlayPauseButton_WhenPlaying_ShowsPause() {
2: guard​ ​let​ playerVC = playerVC ​else​ {
3: XCTFail​(​"Couldn't load player scene"​)
4: return
5:  }
6:  playerVC.handlePlayPauseTapped(​self​)
7: XCTAssertEqual​(​"Pause"​,
8:  playerVC.playPauseButton.title(for: .normal))
9: }

Our testPlayerPlayPauseButton_WhenPlaying_ShowsPause starts with the same guard let as in the previous test, either fetching the playerVC or failing the test immediately. On 6, we effectively “tap” the Play/Pause button by calling the same method that real button taps do. The only difference is that for the sender parameter, we send self, the test class itself. That method takes an AnyObject for this sender parameter, and never uses it, so this is fine.

Now that we’ve “tapped” the Play/Pause button, we want to check its title. Recall that in ViewController.swift, we set the title with setTitle(for:), which took a title string and a button “state,” which for us is .normal. So to get the title, we can use a UIButton method called title(for:). And that’s what we do on lines 7-8, asserting that the value we get back from the playPauseButton’s title is Pause.

Run this test by itself, or click the diamond next to the class name to run all tests on the class. Everything passes, and now we see how to interact with the player scene—or to use testing jargon, the system under test—before we assert what we expect its values to be.

images/testing/xcode-two-tests-pass.png
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset