Swift 3
To evaluate student solutions written in Swift, you need to write tests using XCTest.
Basics
Let's start with a basic exercise. Your student has to implement a HelloWorld
class with a function helloWorld()
that returns the string "Hello World". For the sake of simplicity, it doesn't print anything.
Here is an example solution:
import Foundation
class HelloWorld {
func helloWorld() -> String {
return "Hello World"
}
}
And this is an example test of the student's code:
import XCTest
@testable import Base
class Evaluate: UMBaseTestCase {
var hw = HelloWorld()
func testHelloWorld() {
XCTAssertEqual(hw.helloWorld(),
"Hello World",
"You should print'Hello World'")
}
}
extension Evaluate {
static var allTests : [(String, (Evaluate) -> () throws -> Void)] {
return [
("testHelloWorld", testHelloWorld)
]
}
}
Let's examine the above test code.
import XCTest
@testable import Base
Your code must include those lines to import the XCTest
framework and our module.
class Evaluate: UMBaseTestCase {
}
Your test code must live inside a class named Evaluate
. It is, otherwise, just a plain Swift class.
UMBaseTestCase
inherits from XCTestCase
. It provides some additional helper methods to manipulate Standard Input and Output.
The Evaluate
class can inherit from UMBaseTestCase
or directly XCTestCase
.
extension Evaluate {
static var allTests : [(String, (Evaluate) -> () throws -> Void)] {
return [
("testHelloWorld", testHelloWorld)
]
}
}
In order to run your test methods, you must add all of them to allTests
.
XCTAssertEqual
is a method provided by XCTest
which asserts that two expressions have the same value. You can use it with 3 arguments:
Actual value
Expected value
Feedback message to be shown if values are not equal
You can see other assertions here.
Our test class has a test case testHelloWorld()
which asserts equality of "Hello World" and the return value of the student's function.
Testing outputs
Let's change the previous example a little bit and ask students to print "Hello World" rather than return the string.
Here is an example solution:
class HelloWorld {
func helloWorld() {
print("Hello World")
}
}
To be able to test the student code, you must be able to read standard output after calling helloWorld()
. For this purpose, we have provided helper methods on UMBaseTestCase
.
For example:
import XCTest
@testable import Base
class Evaluate: UMBaseTestCase {
var hw = HelloWorld()
func testHelloWorld(){
self.prepareStdOut()
hw.helloWorld()
XCTAssertEqual(self.getOutput(),
"Hello World\n",
"You should print'Hello World'")
}
}
extension Evaluate {
static var allTests : [(String, (Evaluate) -> () throws -> Void)] {
return [
("testHelloWorld", testHelloWorld)
]
}
}
There are a few differences between this test and the previous one:
We are using helper methods to gain access to data written to standard output.
Calling
self.prepareStdOut()
redirects standard output to a temporary log file to be able to read later.Comparing the "Hello World" string with the output that we read with
self.getOutput()
.The
getOutput()
method reads the output from the log file and restores the standard output to previous state, therefore we can get feedback from test results.
You can see more details about helper methods below in other examples.
Providing inputs
How do you test whether students are able to read input from standard input?
Consider that the student's task is to read a customer's first and last name from standard input and print the full name to standard output.
The solution for this problem can be implemented as follows:
import Foundation
class Customer {
var name:String!
func getName() -> String {
print("Enter your name:")
self.name = self.readString()
return self.name
}
private func readString() -> String {
let str = String(data: FileHandle.standardInput.availableData,
encoding:String.Encoding.utf8)!
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
return str
}
}
UMBaseTestCase
has a method to fill standard input with given values.
func setStdInput(input:)
inputs
: String input to be entered to standard input. A line separator will be appended to each input.
The following test class enters name in the console and checks whether the student's code can return the name.
import XCTest
@testable import Base
class Evaluate: UMBaseTestCase {
var customer = Customer()
func testGetName(){
let str = "Albert Einstein"
self.setStdInput(input:str)
var name = customer.getName()
XCTAssertEqual(name,
"Albert Einstein",
"For inputs 'Albert Einstein' it doesn't work.")
}
}
extension Evaluate {
static var allTests : [(String, (Evaluate) -> () throws -> Void)] {
return [
("testGetName", testGetName)
]
}
}
Testing exceptions
You can expect student code to raise exceptions in some cases.
So let's improve the example above and ask students to throw IllegalArgumentException
if the input passed to a function is not valid. setSSN()
won't accept SSNs shorter than 10 characters and setAge()
won't accept negative ages. And if the age is negative, the exception message must be "Age can't be a negative number.".
The solution for this problem can be implemented as follows:
enum CustomerError: Error {
case IllegalArgumentError
}
class Customer {
var SSN:String?
var age:Int?
func setSSN(ssn:String) throws {
if (ssn.characters.count < 10) {
throw CustomerError.IllegalArgumentError
}
self.SSN = ssn
}
func setAge(age:Int) throws {
if (age < 0) {
// Error with message
throw CustomerError.IllegalArgumentError
}
self.age = age
}
}
To check whether the student handled exception cases, you can use XCTAssertThrowsError
. The following test class is checking exceptions.
import XCTest
@testable import Base
class Evaluate: UMBaseTestCase {
var customer = Customer()
func testInvalidSSN() {
// Check if it raises exception for invalid Social Security Number
XCTAssertThrowsError(try customer.setSSN(ssn:"123"),
"You should throw IllegalArgumentError")
}
func testInvalidAge() {
// Check if it raises exception for negative age
XCTAssertThrowsError(try customer.setAge(age:-5),
"You should throw IllegalArgumentError")
}
}
extension Evaluate {
static var allTests : [(String, (Evaluate) -> () throws -> Void)] {
return [
("testInvalidSSN", testInvalidSSN),
("testInvalidAge", testInvalidAge)
]
}
}
You can also use a try
/ catch
block to check whether an exception is thrown and call XCTFail
if it is not.
UMBaseTestCase
Let's see all the details of the UMBaseTestCase
class which is provided by Udemy to help you deal with reading outputs and entering inputs for student solutions.
import XCTest
import Foundation
@testable import Base
class UMBaseTestCase: XCTestCase {
var savedStdOut:Int32!
var savedStdIn:Int32!
let stdInLogFile = "/eval/stdIn.log"
let stdOutLogFile = "/eval/stdOut.log"
var isStdOutRedirected:Bool!
var isStdInRedirected:Bool!
override func setUp() {
super.setUp()
isStdOutRedirected = false
isStdInRedirected = false
}
override func tearDown(){
super.tearDown()
if(isStdOutRedirected == true){
self.restoreStdOutToDefault()
}
if(isStdInRedirected == true){
self.restoreStdInToDefault()
}
}
//Redirect StandardInput in order to read from log file
func setStdInput(input:String){
do {
try input.write(toFile:stdInLogFile, atomically: true, encoding: String.Encoding.utf8)
} catch {
// failed to write file
}
isStdInRedirected = true
savedStdIn = dup(STDIN_FILENO)
freopen(stdInLogFile, "r", stdin)
}
//Redirect StandardOutput to log file
func prepareStdOut() {
isStdOutRedirected = true
savedStdOut = dup(STDOUT_FILENO)
freopen(stdOutLogFile, "w", stdout)
}
//Read from StandardOutput from log file
func getOutput()->String {
//Restrore standartOutput
//This is important to restore to get Test results
self.restoreStdOutToDefault()
let content = try! String(contentsOfFile: stdOutLogFile, encoding: String.Encoding.utf8)
return content
}
private func restoreStdOutToDefault(){
isStdOutRedirected = false
fflush(stdout)
dup2(savedStdOut, STDOUT_FILENO)
close(savedStdOut)
}
private func restoreStdInToDefault(){
isStdInRedirected = false
dup2(savedStdIn, STDIN_FILENO)
close(savedStdIn)
}
}
Last updated