简体   繁体   中英

Objective C - Best practice for Unit Testing with multiple test inputs

I'm writing unit test codes for an existing project. The project is in Objective-C and I have to test few functions with a number of inputs to the test cases. For example, I have a test case to test a function calculator where two parameters are inputted. Currently I create array to store the set of input values to run the test. The code used are as follows:

- (void)setUp {
    [super setUp];
    self.vcToTest = [[BodyMassIndexVC alloc] init];
    input1 = [[NSMutableArray alloc] initWithObjects:@"193", @"192", @"192", @"165", @"155", @"154", nil];
    input2 = [[NSMutableArray alloc] initWithObjects:@"37", @"37", @"36", @"80",@"120", @"120", nil];
}



- (void)testCalculatorSuccess {
    for (int i=0; i<input1.count; i++) {
        NSArray *expectedResult = [[NSArray alloc] initWithObjects: @"9.93", @"10.04", @"9.77", @"29.38", @"49.95", @"50.60", nil];
        NSString *actualResult = [self.vcToTest calculateResult:input1[i] andInput2:input2[i]];
        XCTAssertEqualObjects(actualResult, expectedResult[i]);
    }

}

I searched for best practices online but was not able to find any. Can someone help me with this? Am I running the test in the right way? What is the best practice to be followed in such cases? Should I create a test case for every set of input?

Your test class should be targeting one specific thing to test which is the sut (system under test). In your case the sut variable should be your vcToTest. The way I would go about testing is by dependency injection rather than storing all the arrays you are testing as instance variables. So you would create a test method that takes in the parameters that you want to test. This way you don't need to keep creating instance variables, you only create local variables that are relevant to the test method.

- (void)setUp {
    [super setUp];
    // Put setup code here. This method is called before the invocation of each test method in the class.
    self.sut = [[BodyMassIndexVC alloc] init];
}

- (void)tearDown {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
    self.sut = nil;
    [super tearDown];
}

- (void)testBMIFormulaInCmAndKgSuccess {
    // local variables that are only seen in this method 
    NSArray *heightsInMetres = [[NSMutableArray alloc] initWithObjects:@"193", @"192", @"192", @"165", @"155", @"154", nil];
    NSArray *weightsInKg = [[NSMutableArray alloc] initWithObjects:@"37", @"37", @"36", @"80",@"120", @"120", nil];
    NSArray *expectedResults = [[NSArray alloc] initWithObjects: @"9.93", @"10.04", @"9.77", @"29.38", @"49.95", @"50.60", nil];
    for (int i=0; i<heightsInMetres.count; i++) {
        [self xTestHeightInMeters:heightsInMetres[i] weightInKg:weightsInKg[i] expecting:expectedResults[i]];
    }
}

// the variables that are used to test are injected into this method
- (void)xTestHeightInMeters:(NSString *)height weightInKg:(NSString *)weight expecting:(NSString *)expectedResult {
    NSString *result = [self.sut calculateBMIforHeight:height andWeight:weight];
    XCTAssertEqual(result, expectedResult);
}

If I was you I wouldn't create arrays to run the tests. Arrays are messy and become hard to understand what is going on, and easy to make mistakes. I would create specific test methods that test one thing to make sure the sut is working properly. For example in TDD you create a test method that will fail, then modify your sut to fix this failure in the most simple way. Usually this means your fix will just return exactly what you are expecting. Then you make another test that tests the exact same thing with a different value, it should now fail because your sut is simply returning what they previous test was looking for. Now you modify your sut again to make both tests pass. After this in most situations you won't need any addition tests since it has proven to work in two unique ways.

I know you said you are testing software that was written already, but I strongly recommend you check out Test Driven Development. Even if you don't actually apply TDD, you will learn how to create meaningful tests. This helped me learn tdd

In my experience, the key consideration should be how easy it will be to maintain the test suite over time. Your current approach will cause problems down the road in two ways:

  1. If you want to use different numbers for you BMI calculation you need to use a different test class (because you're locking in the values in the setup method).

  2. If you ever decide to use different math or multiple BMI equations you'll have to update the arrays anywhere you're checking those values.

What I would suggest is to instead create a CSV or plain text file that has the height, weight, and expected BMI values in it. That keeps the test data together. Then in your test methods, load the file and check your actual BMI against the expected BMI.

You have the flexibility here to mix and match test data, or use different test files for different BMI equations. Personally, I also like the fact that you can keep old data files around as you change things, in case you ever want to rollback or add legacy algorithm support.

A quick and dirty version would look something like this:

- (NSArray *)dataFromFileNamed:(NSString *)filename {
    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:filename ofType:nil];
    // load the data however its formatted
    // return the data as an array
    return loadedData;
}

- (void)testBMIFormulaInCmAndKgSuccess {
    NSArray *testData = [self dataFromFileNamed:@"BMI_data_01.txt"];
    for (int i=0; i < testData.count; i++) {
        NSArray *dataSet = testData[i];
        CGFloat height = dataSet[0];
        CGFloat weight = dataSet[1];
        CGFloat expectedBMI = dataSet[2];
        NSString *actualResult = [self.vcToTest calculateBMIforHeight:height andWeight:weight];
        XCTAssertEqual(actualResult, expectedBMI);
    }
}

It's usually best to avoid for-loops in unit test code. This rule usually leads to separate assertions.

But in your case, you want to exercise a function with various inputs. Your approach is not bad at all. We can simplify the arrays by using literals:

NSArray<NSString *> *heightsInMetres = @[ @"193",   @"192",  @"192",   @"165",   @"155",   @"154"];
NSArray<NSString *> *weightsInKg =     @[  @"37",    @"37",   @"36",    @"80",   @"120",   @"120"];
NSArray<NSString *> *expectedResults = @[@"9.93", @"10.04", @"9.77", @"29.38", @"49.95", @"50.60"];

I also normally avoid funny formatting. But aligning the columns helps us see the values in a table-like format.

Finally, I wouldn't put these values in setUp unless they're used across multiple tests.

If you need many tests like this, it may be worth exploring a test format that uses actual tables, like Fitnesse . Table data could be in spreadsheets or in wiki format, driving tests.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM