是否可以在没有Xcode的情况下使用XCTest单元测试?

是否可以在没有Xcode的情况下使用XCTest单元测试?,xcode,xctest,Xcode,Xctest,我想实现XCTest单元测试,但实际上不想使用XCode来实现。如何做到这一点并不明显,我想知道这是否可能 从目前为止我所发现的情况来看,人们可能会觉得XCTest完全依赖于XCode。我已经找到了命令行支持,但这取决于找到一个XCode项目或工作区 我在这里有什么选择吗,还是干脆删除现有的SenTestingKit代码,恢复到一些自制的单元测试代码?我手头有一些这样的代码,但这不是正确的做法 理由/历史: 这不仅仅是我是个老笨蛋。我有一个Objective-C程序,我上一次接触它是在两年前,

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

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

我在这里有什么选择吗,还是干脆删除现有的
SenTestingKit
代码,恢复到一些自制的单元测试代码?我手头有一些这样的代码,但这不是正确的做法


理由/历史:

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


此代码不是使用XCode开发的,因此没有与之关联的
.project
文件,到目前为止,使用SentTestingKit的主程序和Makefile
check
目标可以愉快地管理测试(部分原因是旧的skool,再次,部分原因是缺乏对IDE的喜爱,部分原因是这是对Objective-C的实验,所以最初坚持我所知道的)。

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

要获得完整的非Xcode解决方案,请继续阅读

几年前我曾问过类似的问题:但从那以后情况发生了变化

随着时间的推移,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;
}

不要期望代码能够编译,但它应该会给你一个想法。如果你有任何问题,请随时提问。也可以按照XTest framework的标题了解更多关于它的类及其文档的信息。

谢谢@stanislaw pankevich提供了一个很好的答案。为了完整性,我在这里加入了(或多或少)我最终完成的完整测试程序,包括一些额外的细节和注释

(我认为这是一个完整的程序,因为它测试功能 在
util.h
中定义,此处不包括)

文件
UtilTest.h

#import <XCTest/XCTest.h>
@interface UtilTest : XCTestCase
@end
驱动程序
runtests.m
(这是主程序,makefile实际调用它来运行所有测试):

#导入“UtilTest.h”
#进口
//定义我的观察对象——我只需要在一个地方做这个
@接口brownetestobservation:NSObject
@属性(赋值,非原子)NSU整数测试失败;
@属性(赋值,非原子)NSUTEGER TestScaled;
@结束
@实现布朗观测
-(instancetype)初始化{
self=[super init];
self.testsFailed=0;
回归自我;
}
//我们可以在这里添加各种其他功能,以便了解
//各种事件:请参见
// https://developer.apple.com/reference/xctest?language=objc
-(void)testSuiteWillStart:(XCTestSuite*)testSuite{
NSLog(@“套件%@…,[testSuite名称]);
self.testscaled=0;
}
-(void)testsuiteddfinish:(XCTestSuite*)testSuite{
NSLog(@“…套件%@(%tu测试)”,[testSuite名称],self.testscaled);
}
-(void)testCaseWillStart:(XCTestSuite*)testCase{
NSLog(@“测试用例:%@,[testCase名称]);
self.testscaled++;
}
-(void)testCase:(XCTestCase*)testCase didFailWithDescription:(NSString*)description inflee:(NSString*)filePath atLine:(nsinteger)lineNumber{
NSLog(@“失败:%@,%@(%@:%tu)”,测试用例,描述,文件路径,行号);
self.testsFailed++;
}
@结束
int main(int argc,字符**argv){
XCTestObservationCenter*center=[XCTestObservationCenter sharedTestObservationCenter];
Brownetestobservation*observator=[Brownetestobservation new];
[中心添加测试观察者:观察者];
类类[]={[UtilTest类],};//在此处添加其他类
int nclasses=sizeof(classes)/sizeof(classes[0]);
对于(int i=0;i 0){
NSLog(@“运行测试:%tu失败”,observer.tests失败);
rval=1;
}
返回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
FRAMEWORKS=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/FRAMEWORKS
TESTCASES=UtilTest
%.o:%.m
clang-F$(框架)-c$<
检查:运行测试
./runtests 2>runtests.stderr
runtests:runtests.o$(测试用例=0)…/libmylib.a
cc-o$@$<-framework Cocoa-F$(框架)-rpath$(框架)\
-框架XCTest$(测试用例:=.o)-L..-lmylib
注:

  • XCTestObservation
    类现在已被弃用,并被
    XCTestObservation
    取代
  • 测试结果被发送到一个共享的XCTestObservationCenter,不幸的是,它会干扰stderr(因此必须重定向到其他地方)–似乎无法避免这种情况,只能将它们发送到我的观察中心。在我的实际程序中,我用一个函数替换了
    runtests.m
    中的
    NSLog
    调用,该函数会向stdout发出颤音,因此我可以将其与发送到默认观察中心的颤音区分开来
  • 另见 (假设您使用的是XCode)
  • …这个
  • …以及(eg)
    /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework/headers中文件标题中的注释
这一个工作正常:

  • 像往常一样构建你的.xctest目标。默认情况下,它将添加到主机应用程序的插件中,但位置无关
  • 使用co创建一个运行器命令行工具
    #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
    
    #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
    
    #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;
      }
    }