【问题标题】:Is it possible to use XCTest unit tests without Xcode?是否可以在没有 Xcode 的情况下使用 XCTest 单元测试?
【发布时间】:2017-05-18 00:12:03
【问题描述】:

我想实现 XCTest 单元测试,但不想使用 XCode 来实现。如何做到这一点并不明显,我想知道这是否可能?

从我目前发现的情况来看,人们可能会觉得 XCTest 完全依赖于 XCode。我找到了xcodebuild test 命令行支持,但这取决于找到 XCode 项目或工作区。

我在这里有什么选择吗,还是我只是撕掉现有的SenTestingKit 代码并恢复为一些自制的单元测试代码?我手头有一些这样的代码,但这不是正确的做法。


基本原理/历史:

这不仅仅是我老了。我有一个 Objective-C 程序,我上次接触是两年前,为此我基于SenTestingKit 开发了一套合理的单元测试。现在我回到这段代码——我可能至少必须重建这件事,因为干预库的变化——我发现SenTestingKit 已经消失,被 XCTest 取代。哦,好吧....

此代码不是使用 XCode 开发的,因此没有与之关联的 .project 文件,到目前为止,测试都使用 SenTestingKit 的主程序和 Makefile check 目标(部分是又是老派,部分原因是对 IDE 缺乏喜爱,部分原因是对 Objective-C 的实验,所以最初坚持我所知道的)。

【问题讨论】:

    标签: xcode xctest


    【解决方案1】:

    如果您正在寻找基于 Xcode 的解决方案,请参阅 this 及其链接的解决方案以获取示例。

    有关完整的非基于 Xcode 的解决方案,请继续阅读。

    几年前我曾经问过类似的答案:Is there any non-Xcode-based command line unit testing tool for Objective-C?,但从那以后情况发生了变化。

    随着时间的推移,XCTest 中出现的一个有趣功能是能够运行您的自定义测试套件。我曾经成功地实现它们以满足我的研究需要,这是一个示例代码,它是一个命令行 Mac OS 应用程序:

    @interface FooTest : XCTestCase
    @end
    
    @implementation FooTest
    - (void)testFoo {
      XCTAssert(YES);
    }
    - (void)testFoo2 {
      XCTAssert(NO);
    }
    
    @end
    
    @interface TestObserver : NSObject <XCTestObservation>
    @property (assign, nonatomic) NSUInteger testsFailed;
    @end
    
    @implementation TestObserver
    
    - (instancetype)init {
      self = [super init];
    
      self.testsFailed = 0;
    
      return self;
    }
    
    - (void)testBundleWillStart:(NSBundle *)testBundle {
      NSLog(@"testBundleWillStart: %@", testBundle);
    }
    
    - (void)testBundleDidFinish:(NSBundle *)testBundle {
      NSLog(@"testBundleDidFinish: %@", testBundle);
    }
    
    - (void)testSuiteWillStart:(XCTestSuite *)testSuite {
      NSLog(@"testSuiteWillStart: %@", testSuite);
    }
    
    - (void)testCaseWillStart:(XCTestCase *)testCase {
      NSLog(@"testCaseWillStart: %@", testCase);
    }
    
    - (void)testSuiteDidFinish:(XCTestSuite *)testSuite {
      NSLog(@"testSuiteDidFinish: %@", testSuite);
    }
    
    - (void)testSuite:(XCTestSuite *)testSuite didFailWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber {
      NSLog(@"testSuite:didFailWithDescription:inFile:atLine: %@ %@ %@ %tu",
            testSuite, description, filePath, lineNumber);
    }
    
    - (void)testCase:(XCTestCase *)testCase didFailWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber {
      NSLog(@"testCase:didFailWithDescription:inFile:atLine: %@ %@ %@ %tu",
            testCase, description, filePath, lineNumber);
      self.testsFailed++;
    }
    
    - (void)testCaseDidFinish:(XCTestCase *)testCase {
      NSLog(@"testCaseWillFinish: %@", testCase);
    }
    
    @end
    
    int RunXCTests() {
      XCTestObserver *testObserver = [XCTestObserver new];
    
      XCTestObservationCenter *center = [XCTestObservationCenter sharedTestObservationCenter];
      [center addTestObserver:testObserver];
    
      XCTestSuite *suite = [XCTestSuite defaultTestSuite];
    
      [suite runTest];
    
      NSLog(@"RunXCTests: tests failed: %tu", testObserver.testsFailed);
    
      if (testObserver.testsFailed > 0) {
        return 1;
      }
    
      return 0;
    }
    

    要编译此类代码,您需要显示 XCTest 所在文件夹的路径,例如:

    # in your Makefile
    clang -F/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks XCTestDriver.m
    

    不要期望代码可以编译,但它应该会给你一个想法。如果您有任何问题,请随时询问。还可以关注 XCTest 框架的标题,以了解有关其类及其文档的更多信息。

    【讨论】:

    • 谢谢@stanislaw-pankevich - 这看起来非常有用。我仍在研究头文件中的文档,但可能会带着问题返回。有了那个指针,我还找到了the XCTest docs,我还在为那些事摸不着头脑……
    【解决方案2】:

    感谢@stanislaw-pankevich 的精彩回答。在这里,为了完整起见,我将(或多或少)包括我最终完成的完整测试程序,其中包括一些额外的细节和 cmets。

    (从我的角度来看,这是一个完整的程序,因为它测试功能 在util.h中定义,这里不包含)

    文件UtilTest.h

    #import <XCTest/XCTest.h>
    @interface UtilTest : XCTestCase
    @end
    

    文件UtilTest.m

    #import "UtilTest.h"
    #import "../util.h"  // the definition of the functions being tested
    
    @implementation UtilTest
    
    // We could add methods setUp and tearDown here.
    // Every no-arg method which starts test... is included as a test-case.
    
    - (void)testPathCanonicalization
    {
        XCTAssertEqualObjects(canonicalisePath("/p1/./p2///p3/..//f3"), @"/p1/p2/f3");
    }
    @end
    

    驱动程序runtests.m(这是主程序,makefile 实际调用它来运行所有测试):

    #import "UtilTest.h"
    #import <XCTest/XCTestObservationCenter.h>
    
    // Define my Observation object -- I only have to do this in one place
    @interface BrownieTestObservation : NSObject<XCTestObservation>
    @property (assign, nonatomic) NSUInteger testsFailed;
    @property (assign, nonatomic) NSUInteger testsCalled;
    @end
    
    @implementation BrownieTestObservation
    
    - (instancetype)init {
        self = [super init];
        self.testsFailed = 0;
        return self;
    }
    
    // We can add various other functions here, to be informed about
    // various events: see XCTestObservation at
    // https://developer.apple.com/reference/xctest?language=objc
    - (void)testSuiteWillStart:(XCTestSuite *)testSuite {
        NSLog(@"suite %@...", [testSuite name]);
        self.testsCalled = 0;
    }
    
    - (void)testSuiteDidFinish:(XCTestSuite *)testSuite {
        NSLog(@"...suite %@ (%tu tests)", [testSuite name], self.testsCalled);
    }
    
    - (void)testCaseWillStart:(XCTestSuite *)testCase {
        NSLog(@"  test case: %@", [testCase name]);
        self.testsCalled++;
    }
    
    - (void)testCase:(XCTestCase *)testCase didFailWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber {
        NSLog(@"  FAILED: %@, %@ (%@:%tu)", testCase, description, filePath, lineNumber);
        self.testsFailed++;
    }
    
    @end
    
    int main(int argc, char** argv) {
        XCTestObservationCenter *center = [XCTestObservationCenter sharedTestObservationCenter];
        BrownieTestObservation *observer = [BrownieTestObservation new];
        [center addTestObserver:observer];
    
        Class classes[] = { [UtilTest class], };  // add other classes here
        int nclasses = sizeof(classes)/sizeof(classes[0]);
    
        for (int i=0; i<nclasses; i++) {
            XCTestSuite *suite = [XCTestSuite testSuiteForTestCaseClass:classes[i]];
            [suite runTest];
        }
    
        int rval = 0;
        if (observer.testsFailed > 0) {
            NSLog(@"runtests: %tu failures", observer.testsFailed);
            rval = 1;
        }
    
        return rval;
    }
    

    生成文件:

    FRAMEWORKS=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks
    TESTCASES=UtilTest
    
    %.o: %.m
        clang -F$(FRAMEWORKS) -c $<
    
    check: runtests
        ./runtests 2>runtests.stderr
    
    runtests: runtests.o $(TESTCASES:=.o) ../libmylib.a
        cc -o $@ $< -framework Cocoa -F$(FRAMEWORKS) -rpath $(FRAMEWORKS) \
            -framework XCTest $(TESTCASES:=.o) -L.. -lmylib
    

    注意事项:

    • XCTestObserver 类现已弃用,取而代之的是 XCTestObservation
    • 测试结果被发送到一个 shared XCTestObservationCenter,不幸的是,它会分散注意力到 stderr(因此必须重定向到其他地方)——似乎不可能避免这种情况并将它们发送到我的观察中心。在我的实际程序中,我将 runtests.m 中的 NSLog 调用替换为与标准输出聊天的函数,因此我可以将其与转到默认观察中心的聊天区分开来。
    • 另见overview documentation (假设您使用的是 XCode),
    • ...XCTest API documentation,
    • ...以及文件标题中的注释(例如)/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework/Headers

    【讨论】:

      【解决方案3】:

      这个工作正常:

      1. 像往常一样构建您的 .xctest 目标。默认情况下,它将添加到宿主应用程序的插件中,但位置无关紧要。
      2. 使用以下代码创建运行器命令行工具。 更新:Xcode 在 /Applications/Xcode.app/Contents/Developer/usr/bin/xctest 中提供了相当独立的运行器 如果您不想创建自己的简单跑步者,可以使用这个。

      3. 使用测试套件的完整路径运行该工具。

      示例运行器代码:

      #import <Foundation/Foundation.h>
      #import <XCTest/XCTest.h>
      
      int main(int argc, const char* argv[]) {
        @autoreleasepool {
          XCTestSuite *suite = [XCTestSuite testSuiteForBundlePath:
            [NSString stringWithUTF8String:argv[1]]];
          [suite runTest];
      
          // Note that XCTestSuite is very shy in terms of errors,
          // so make sure that it loaded anything indeed:
          if (!suite.testRun.testCaseCount) return 1;
      
          return suite.testRun.hasSucceeded;
        }
      }
      

      【讨论】:

        猜你喜欢
        • 2012-02-24
        • 1970-01-01
        • 2014-01-28
        • 1970-01-01
        • 2011-02-25
        • 2023-01-09
        • 2022-08-22
        • 1970-01-01
        • 2018-05-28
        相关资源
        最近更新 更多