قالب وردپرس درنا توس
Home / IOS Development / IOS functional testing with user histories, UI Test and local server

IOS functional testing with user histories, UI Test and local server



In my projects, I have mostly focused on Unit Testing, but I was always interested in performing some tests on the navigation flow in the application, as I could confirm something like this: "When a user enters valid credentials and cranes, the Login button , then he moves to the next view ". I know that at first glance it may sound incredibly clear … you've written the navigation so you know it's going to behave like this. Sure. Trust me (but you already know) things happen, your obvious power can suddenly slow down and things work differently than your expectations. Yes, I know, it's a terrible prospect: that's why writing some function tests is a good idea!

Function Tests and something about User Stories

A functional test is a good way to verify an implemented user history. A story is essentially a well-defined task; In extreme programming (but also in somewhat less stringent environments), you use stories to describe all possible user cases that may occur in a project. You (or your PM) use a list of stories to share project scenarios, in small, specific, logical sections.
Take something similar to the sentence we previously introduced:

"I'm signed in,
when I enter valid ID,
and I click the" Sign in "button,
I'm moving to the list view. "

This is a good example of a story that gives you information about a really specific task. A very important thing to understand when implementing a story is that you have to focus only on that story. For example, if you implement the previous one, you will not implement the logic needed for invalid credentials (like displaying an error message), just the one described in the current story, it's probably another story to verify "Invalid ID" behavior .

As said, a functional test can be regarded as an automation of a flow, and to be "green" (successful), it must run from start to finish, respecting the logic described by history.

The example project

The project is extremely simple. We want to create a program that allows the user to log in to a restricted area, see a list of robots and a detail view for each of them (For convenience, we exceed the user registration).

These are some useful stories to describe the project:

Login with Invalid ID
"Given that I am a user,
and I am in Log In,
] When I Entering a valid ID,
and I click the Login button,
happens nothing "

Sign in with valid ID
" Given that I am a user,
and I'm in the login view,
when I enter valid ID,
and I click the Login button,
I see the Robot List

Robot List of Robots
] "I'm a logged in user,
and I'm in the Robot List view,
and I own some robots,
I see my robot"

Robot List Without robots
"Given that I'm a logged in user,
and I'm in the Robot List,
list and I do not own n [1

9659000] Robot Details
"Given that I'm a logged-in user,
and I'm in detail about robot details,
I see the robot name, id Sign Out
"I'm a logged in user,
and I'm in the Robot List,
list when I press the Logout button,
Then I'll Sign Out Page and
I'm Signed Out

Let's Code

Before proceeding with the article I suggest downloading the code for this project. There you will find all these stories already implemented in a simple application.

Injecting Fake Data

A really common way of performing tests is to fax server responses, looking for a predefined stream. For example, when I want to simulate a valid login, I can inject a server response with a 200 or 201 status code containing an auth token, while if I want to simulate an invalid ID, I can set the status of 403 or 401 and return an error code. Then, the application will continue its flow in different ways depending on fake data. So we can implement tests to verify the behavior of the application is correct for all the flows we can generate with fake data.

When you work with Unit Tests, you can fake server response in different ways. You can stump HTTP responses that capture HTTP conversations (the OHTTPStubs library is a very good choice to implement this solution) or you can hide your network library to return false data (this topic is worth a dedicated article).

The new Xcode UI test target works differently: tests and applications run as different instances, meaning you do not have the ability to inject data directly from the test code. A really common solution for injecting some data under UI testing starts the program with dedicated launch arguments and generates another data stream when the app is running in "test mode".

If you open the file TB_UITestingUITests.swift you will find that in the setUp feature, the application example is launched with the argument UI_TESTING_MODE :

 
  la app = XCUIAapplication ()
app.launchArguments = [“UI_TESTING_MODE”]
app.launch ()

This information is then retrieved from the application thanks to the NSProcessInfo class. In the Helper.swift file, the endPoint function is responsible for returning the server address. This is a good point to inject false information pointing the application to another server, in this case localhost: 4567 the URL where our fake server responds to generating data for the tests.

  func endPoint () -> NSURL {

if (NSProcessInfo.processInfo (). arguments.contains ("UI_TESTING_MODE")) {
return NSURL (string: "http: // localhost: 4567")!
}else{
return NSURL (string: "http://www.example.com/api/")!
}
}

If you do not want to use a fake server, you can also drop data directly into the application. Personally, I do not like this method since the application code requires too many adjustments. With the previous example, we will change only one function, all necessary information will be handled outside of the application code.

Network Requests and the False Sinatra Server

In this article I use Sinatra since it is extremely easy to write and explain the required code. Obviously, we can replace this solution with Node.js, PHP or what you like most.

Just to be consistent with the previous examples, let's focus on the sing-in conversation. Here is the code of the application responsible for it:

  ...
add API = APIService (endPoint: endPoint ())
...
@IBAction func signIn () {
// Verify user login
alert leave username = username_textfield.text,
leave password = password_textfield.text other {
// Error processing here ...
return
}
// Create the call
was signInCall = APICall (path: "session" method: Method.POST)
signInCall.parameters = [“username”:username, “password”:password]

// Handle the answer
API.request (signInCall) {
(code, JSON) -> Invalid
change code {
case StatusCode.Created:
if let the token = json? [“access_token”] as? string {
NSUserDefaults.accessToken = Token
la vc = instantiateViewController ("RobotList")
self.showViewController (vc, sender: self)
}
case StatusCode.Forbidden:
print ("Login failed")
default:
print ("other error")
}
}
}

You have been organized into three main sections:
It verifies user input, then creates the conversation and eventually handles the answer.

I've written some simple API classes that should be very easy to read (do not use it on real projects, please : P it's not the newest code). The APICall example receives the API path and the HTTP method, in this case "session" and "POST", and specifies usernames and passwords as call parameters internally converted to JSON data.

The class APIService is responsible for sending an API request request and handling the response through callbacks.

In the previous example, an instance of this class is initialized with an endpoint defined at the start time ;) then the incoming call that was previously defined starts and handles the response through a simple switch focuses on the response status code.
If the session is properly created, we save the auth token and we move to the next viewing controls, otherwise nothing happens just like in the previously dedicated stories.

All the other APIs require that this program has been handled in a very similar way, creating an instance of the call and handling the response in a callback addressing another code, depending on the response status.

It's time to take a look at the server code (carry with me, my Ruby skills are limited.)

It's extremely easy to write a simple Sinatra server. Some predefined blocks are named by HTTP methods, and you can route requests with a super easy and readable syntax:

   & # 39;  & # 39;
status 
... your code
end

Here is the full code for the "session" conversation.

  The item "Session & # 39; makes
data = JSON.parse request.body.read
username = data [“username”]
password = data [“password”] # not used in this example ...

case username
when "user_with_robots"
status 201
JSON.generate (: access_token => "1234")
when "user_without_robots"
status 201
JSON.generate (: access_token => "5678")
else
status 403
JSON.generate (: error => "invalid_credentials")
end

end

We get the username and password that comes from the request, and depending on the username, we determine what responses you will create. I found this solution extremely useful, as you will see later, we can write tests that will be addressed to different responses, depending on predefined data. Here for the "session" conversation, you know that the password as username "user_with_robots" or "user_without_robots" gives you valid tokens, otherwise this code simulates invalid credentials, with a 403 response and an error code. As you can imagine that the received symbol will be useful to define the robot list later, generates an empty or a full list depending on the username. Tidy.

You can look at all the other Server Responses defined by the TestServer / server.rb file. The logic behind these calls is exactly the same as described for the "session" conversation.

You can start the server only by performing the rubin script (you may need to install the Sinatra bead).

Force the endPoint program to "localhost: 4567" you can try the server, try to perform a login with "user_with_robots" as your username and password  :)

Implement UI Tests

Great, Now we have a work application and a test server. We can simulate which application flow we need!
My suggestion is to start from the histories listed earlier and just write tests that confirm these stories.
We can focus on the login process, let's implement the "Login with valid credentials" test.

To generate a valid signed user, we can use one of the accepted usernames, let us select "user_with_robots" one and write a really readable feature. I think that the Ruby Feature Naming Style (name_of_the_function) is perfect for achieving good readability, so all the features you see in the TB_UITestingUITests.swift file are written with this approach. Let's focus on the feature test_login_when_credentials_are_valid .

  func test_login_when_credentials_are_valid () {

La app = XCUIAapplication ()
performLogin (app, username: "user_with_robots")

XCTAssert (! App.buttons [“signin”] .exists)
}

func performLogin (app: XCUIAapplication, username: String) {

leave usernameTextField = app.textFields [“Username”]
usernameTextField.tap ()
usernameTextField.typeText (username)

let passwordSecureTextField = app.secureTextFields [“Password”]
passwordSecureTextField.tap ()
passwordSecureTextField.typeText ("middle password")
app.buttons [“signin”] .tap ()
}

This feature should be simple, it maintains a reference to the app, simulates the username and password field, and tapes the login button through the performLogin method. Finally, you confirm that the current view has been changed and checks that the signin button no longer exists.

Conclusions

Even though I can count on a QA team, I have found this process extremely useful during the development of my current project. Performing automated tests with a fake server is extremely useful and if adopted in conjunction with good Unit tests, you get a good coverage! There are many other different ways to do what is explained in this article, so I'd love to hear your opinion and your suggestion on twitter !

Download source




Source link