【ArchSummit】如何通过AIOps推动可量化的业务价值增长和效率提升?>>> 了解详情
写点什么

iOS 开发中的单元测试(三)——URLManager 中的测试用例解析

  • 2013-08-27
  • 本文字数:9646 字

    阅读完需:约 32 分钟

URLManager 是一个基于 UINavigationController 和 UIViewController,以 URL Scheme 为设计基础的导航控件,目的是实现 ViewController 的松耦合,不依赖。

准备框架,定义基类

首先按照之前的两篇文章介绍的方法导入单元测试框架和匹配引擎框架,建立好测试 Target,并配置编译选项。

定义测试用例基类: UMTestCase (代码 1),其他用例全部继承自 UMTestCase。

复制代码
#import <GHUnitIOS/GHTestCase.h>
@interface UMTestCase : GHTestCase
@end

代码 1,UMTestCase,用例基类

构建用例

URLManager 工具类( UMTools )测试用例( UMToolsTestCase )。UMTools 中扩展了 NSURL,NSString 和 UIView,方法涉及到给 URL 添加 QueryString 和从 QueryString 中读取参数,对字符串做子串判断,进行 URL 的编码和解码,对 UIView 的 x,y,width 和 height 的直接读写等。需要在用例中定义测试过程中会使用到属性(代码 2), 并在 setUpClass 中初始化他们(代码 3)。

复制代码
// 普通字符串,带有字母和数字
@property (strong, nonatomic) NSString *string;
// 普通字符串,仅带有字母
@property (strong, nonatomic) NSString *stringWithoutNumber;
// 将被做 URLEncode 的字符串,含有特殊字符和汉字
@property (strong, nonatomic) NSString *toBeEncode;
// 把 toBeEncode 编码后的串
@property (strong, nonatomic) NSString *encoded;
// 普通的 URL,带有 QueryString
@property (strong, nonatomic) NSURL *url;
// 去掉上边一个 URL 的 QueryString
@property (strong, nonatomic) NSURL *noQueryUrl;
// 一个普通的 UIView
@property (strong, nonatomic) UIView *view;

代码 2,定义属性

复制代码
(void)setUpClass
{
self.string = @"NSString For Test with a number 8848.";
self.stringWithoutNumber = @"NSString For Test.";
self.toBeEncode = @"~!@#$%^&*()_+=-[]{}:;\"'<>.,/?123qwe 汉字 ";
self.encoded = @"%7E%21%40%23%24%25%5E%26%2A%28%29_%2B%3D-%5B%5D%
7B%7D%3A%3B%22%27%3C%3E.%2C%2F%3F123qwe%E6%B1%89%E5%AD%97";
self.url = [NSURL URLWithString:@"http://example.com
/patha/pathb/?p2=v2&p1=v1"];
self.noQueryUrl = [NSURL URLWithString:@"http://example.com
/patha/pathb/"];
self.view = [[UIView alloc] initWithFrame:CGRectMake(10.0f,
10.0f, 100.0f, 100.f)];
}
{1}

代码 3,初始化属性

使用单元测试框架中的断言处理简单用例

单元测试是白盒测试,要做到路径覆盖(代码 4)。 对“ContainsString”的测试进行正向和反向两种情况(即 YES 和 NO 两种返回结果)。

复制代码
#pragma mark - UMString
- (void)testUMStringContainsString
{
NSString *p = @"For";
NSString *np = @"BAD";
GHAssertTrue([self.string containsString:p],
@"\"%@\" should contains \"%@\".",
self.string, p);
GHAssertFalse([self.string containsString:np],
@"\"%@\" should not contain \"%@\".",
self.string, p);

代码 4,字符串测试用例

同时单元测试又要对功能负责,因此在路径覆盖之外还要尽量照顾到完整的功能。例如,对 URLEncode 的测试(代码 5),要对尽量全面的特殊字符进行测试,而不是从源码实现中取出枚举的字符。

复制代码
(void)testUrlencode
{
GHAssertEqualStrings([self.toBeEncode urlencode], self.encoded,
@"URLEncode Error.",
self.toBeEncode, self.encoded);
GHAssertEqualStrings([self.encoded urldecode], self.toBeEncode,
@"URLDecode Error.",
self.encoded, self.toBeEncode);
}

代码 5,URLEncode 测试用例

在进行这个测试之前,urlencode 的实现忽视了对“~”的编码,正是由于单元测试用例所取的特殊字符是单独列举,并非从实现枚举中获取,检查出了这个错误。

引入匹配引擎,使用匹配引擎默认规则

前文提到过匹配引擎可以使测试用例中的断言更加丰富,URLManager 的用例中也使用了匹配引擎: OCHamcrest

在此前的介绍中提到,引入 OCHamcrest 可以通过定义“HC_SHORTHAND”来开启匹配引擎的简写模式。因为开启简写模式后匹配规则中的“containsString”规则和上述例子(代码 5)中的“containsString:”方法命名冲突,导致测试程序无法正常运行,所以这个工程直接使用了类似“HC_asserTaht”这样带有 HC 前缀的完整命名。

我建议使用匹配引擎的开发者谨慎开启简写功能,OCHamcrest 的匹配规则简写通常是很常见的单词,非常容易与工程中的类定义或方法定义重名。即使当下没有规则和方法名发生冲突,随着工程代码量的增加,一旦出现命名冲突的情况,重构的成本将非常高。

匹配引擎可以提供更丰富的断言,最简单的例如,URLManager 的 UMURL 扩展支持向一个 URL 上添加参数,对这个方法测试断言就用到了匹配某个字符串是否包含某子串的规则(代码 6)。

复制代码
#pragma mark - UMURL
- (void)testAddParams
{
NSURL *queryUrl = [self.noQueryUrl addParams:@{@"p1":@"v1",@"p2":@"v2"}];
HC_assertThat(queryUrl.absoluteString, HC_containsString(@"p1=v1"));
HC_assertThat(queryUrl.absoluteString, HC_containsString(@"p2=v2"));
}

代码 6,URL 参数测试用例

匹配规则中的陷阱

由于匹配规则的粒度较细,所以对于某些运行结果需要考虑到多种情况,否则正常的结果也可能会断言失败。

例如测试用例期望得到一个空容器(例如:NSArray),而 SDK 则认为这个容器已经没有存在的必要而释放了他,返回的是一个 nil。对 removeAllSubviews 的测试中,对一个 view 调用 removeAllSubviews 方法,期望 view.subviews 为空。在 SDK 6.x 甚至 SDK 7 DP1 之前,都是没问题的,但在 SDK 7 DP3 中,SDK 会把所有清空的容器和对象释放,以回收系统资源。在这种条件下 view.subviews 返回的就是 nil,如果只是做类似 HC_empty() 这样的匹配,断言会失败,所以在断言之前做一个 subviews 属性的空判断(代码 7)。

复制代码
(void)testRemoveAllSubviews
{
UIView *subViewA = [[UIView alloc] init];
UIView *subViewB = [[UIView alloc] init];
[self.view addSubview:subViewA];
[self.view addSubview:subViewB];
HC_assertThat(self.view.subviews, HC_containsInAnyOrder(subViewA, subViewB, nil));
[self.view removeAllSubviews];
if (nil != self.view.subviews) {
HC_assertThat(self.view.subviews, HC_empty());
}
}

代码 7,removeAllSubviews 用例

另外,在默认匹配规则中会有一些容易产生歧义的命名,以 collection 的 containsInAnyOrder 为例:匹配对象是一个 collection 对象(也就是遵循 NSFastEnumeration 协议的对象,NSArray 等),给出若干个匹配规则或元素。期待这个规则匹配该对象是否包含给出的若干元素,且不关心顺序。但在实际测试过程中会发现,这个规则要求给出的元素必须是该 collection 对象的完备集,也就是说要求给出的元素列表和要匹配的容器对象中的元素必须是相等的结合,但允许不关注顺序。

对 UMNavigationController 的测试中,需要判断增加一项 URL Mapping 是否生效,如果使用该匹配规则,就不能单纯判断 config 是否包含增量的 URL,要断言成功必须连同此前 config 属性初始化写入的值一起考虑,使用一个完整的元素集合进行匹配(代码 8)。

复制代码
(void)testAddConfig
{
[UMNavigationController setViewControllerName:@"ViewControllerA" forURL:@"<br></br>um://viewa2"];
NSMutableDictionary *config = [UMNavigationController config];
NSLog(@"%@", [config allKeys]);
HC_assertThat([config allKeys],
HC_containsInAnyOrder(HC_equalTo(@"um://viewa2"), HC_equalTo(@"<br></br>um://viewa"),
HC_equalTo(@"um://viewb"), nil));
GHAssertEqualStrings(config[@"um://viewa2"], @"ViewControllerA",
@"config set error.");
}

代码 8,AddConfig 用例

自建匹配规则

上述例子表明匹配规则往往无法恰好满足测试需求,需要对默认规则进行升级。

升级一个匹配规则,首先阅读 OCHamcrest 默认规则源码,找到无法满足需求的代码。上述 HC_containsInAnyOrder 的例子中,个性需求是某个 collection 是否包含某几个元素(而非完整集合),而默认规则只能匹配完整集合。阅读源码(代码 9)可以发现,在 maches:describingMismatchTo: 函数中,对规则对象的 collection 属性(要进行匹配的容器对象)进行遍历,并逐个调用 matches: 方法。matches: 方法中针对每个 collection 属性中的元素遍历匹配规则集合(matchers),并从规则集合(matchers)中移除匹配成功的规则。当给出的规则集合(matchers)全部成功匹配过之后,matchers 属性已经为空。若此时对 collection 属性的遍历继续进行,matches: 方法就不会进入匹配逻辑,直接跳出循环返回 NO,导致匹配失败。

复制代码
(BOOL)matches:(id)item
{
NSUInteger index = 0;
for (id<HCMatcher> matcher in matchers)
{
if ([matcher matches:item])
{
[matchers removeObjectAtIndex:index];
return YES;
}
++index;
}
[[mismatchDescription appendText:@"not matched: "] appendDescriptionOf:item];
return NO;
}
- (BOOL)matches:(id)collection describingMismatchTo:(id<HCDescription>)<br></br>mismatchDescription
{
if (![collection conformsToProtocol:@protocol(NSFastEnumeration)])
{
[super describeMismatchOf:collection to:mismatchDescription];
return NO;
}
HCMatchingInAnyOrder *matchSequence =
[[HCMatchingInAnyOrder alloc] initWithMatchers:matchers
mismatchDescription:mismatchDescription];
for (id item in collection)
if (![matchSequence matches:item])
return NO;
return [matchSequence isFinishedWith:collection];
}

代码 9,HC_containsInAnyOrder 规则中的两个核心方法

我们的需求是,当匹配规则列表全部成功匹配之后就是此次匹配成功的标志。所以需要修改 matches: 方法中的匹配逻辑,当匹配列表为空则返回 YES。

升级方案是继承 HCIsCollectionContainingInAnyOrder 创建一个新的匹配规则类 HCIsCollectionHavingInAnyOrder;重新定义匹配规则 HC_hasInAnyOrder;重写调用 matches: 方法的 matches:describingMismatchTo: 方法(代码 10);更新的核心是定义一个 HCMatchingInAnyOrderEx 类,按照个性需求定义 matches: 方法(代码 11)。使用这个修改过的匹配规则就可以判断一个 Collection 是否包含某个几个元素了。

复制代码
@implementation HCIsCollectionHavingInAnyOrder
- (BOOL)matches:(id)collection describingMismatchTo:(id<HCDescription>)<br></br>mismatchDescription
{
if (![collection conformsToProtocol:@protocol(NSFastEnumeration)])
{
[super describeMismatchOf:collection to:mismatchDescription];
return NO;
}
HCMatchingInAnyOrderEx *matchSequence =
[[HCMatchingInAnyOrderEx alloc] initWithMatchers:matchers
mismatchDescription:mismatchDescription];
for (id item in collection)
if (![matchSequence matches:item])
return NO;
return [matchSequence isFinishedWith:collection];
}
@end
id<HCMatcher> HC_hasInAnyOrder(id itemMatch, ...)
{
NSMutableArray *matchers = [NSMutableArray arrayWithObject:HCWrapInMatcher<br></br>(itemMatch)];
va_list args;
va_start(args, itemMatch);
itemMatch = va_arg(args, id);
while (itemMatch != nil)
{
[matchers addObject:HCWrapInMatcher(itemMatch)];
itemMatch = va_arg(args, id);
}
va_end(args);
return [HCIsCollectionHavingInAnyOrder isCollectionContainingInAnyOrder:matchers];
}

代码 10,HCIsCollectionHavingInAnyOrder 实现

复制代码
(BOOL)matches:(id)item
{
NSUInteger index = 0;
BOOL matched = (0 >= [self.matchers count]);
for (id<HCMatcher> matcher in self.matchers)
{
if ([matcher matches:item]) {
[self.matchers removeObjectAtIndex:index];
matched = YES;
return YES;
}
++index;
}
return matched;
}

代码 11,更新过的 matches: 方法

复制代码
(void)testAddConfig
{
[UMNavigationController setViewControllerName:@"ViewControllerA" forURL:@"um://<br></br>viewa2"];
NSMutableDictionary *config = [UMNavigationController config];
HC_assertThat([config allKeys],
HC_hasInAnyOrder(HC_equalTo(@"um://viewa2"), nil));
GHAssertEqualStrings(config[@"um://viewa2"], @"ViewControllerA",
@"config set error.");
}

代码 12,使用新规则的测试用例

另一个方面,在测试过程中会出现各种逻辑,有时默认规则根本无法覆盖,需要完全自建规则。例如对 CGPoint 和 CGSize 的相等匹配,如代码 13 中对 UMView 的 size 和 origin 方法测试。OCHamcrest 的默认规则中根本没有提供任何针对 CGPoint 和 CGSize 两个结构体的匹配规则,所以要完成这个测试就需要自己定义针对这两种数据结构的匹配规则。

复制代码
#pragma mark - UMView
HC_assertThat(NSStringFromCGSize(self.view.size),
HC_equalToSize(self.view.frame.size));
HC_assertThat(NSStringFromCGPoint(self.view.origin),
HC_equalToPoint(CGPointMake(self.view.frame.origin.x, self.<br></br>view.frame.origin.y)));

代码 13,UMView 测试用例片段

自定义匹配规则的详细说明可以参见上一篇《iOS 开发中的单元测试(二)》,本文只对开发自定义规则中遇到的问题和需要特殊处理的方面进行解释。

OCHamcrest 的匹配规则要求被匹配的必须是一个有强引用的对象,所以当被匹配的是一个 struct 结构(如 CGPoint)需要进行一次转换,如代码 14 中定义的这个规则扩展——OBJC_EXPORT id HC_equalToPoint(CGPoint point)。 在 CGPoint 相等匹配的规则中,需要先把 CGPoint 转为字符串后传入断言方法,规则会把这个字符串储存起来,并与后续给出的 CGPoint 进行比较。匹配引擎对传入的需要进行匹配的参数类型没做任何限制,所以规则可以直接传入 CGPoint。

开发自定义规则一般建议同时定义 SHORTHAND,即使当前单元测试中不会用到(例如本文中的测试),但这个规则被其他复用的时候,可能会用到 SHORTHAND 命名。

复制代码
#import <OCHamcrestIOS/HCBaseMatcher.h>
OBJC_EXPORT id<HCMatcher> HC_equalToPoint(CGPoint point);
#ifdef HC_SHORTHAND
#define equalToPoint HC_equalToPoint
#endif
@interface HCIsEqualToPoint : HCBaseMatcher
+ (id)equalToPoint:(CGPoint)point;
- (id)initWithPoint:(CGPoint)point;
@property (nonatomic, assign) CGFloat x;
@property (nonatomic, assign) CGFloat y;
@end

代码 14,扩展匹配规则 HC_equalToPoint 定义

在匹配规则的过程中,有一个点需要特别注意,即对匹配对象类型和完整性的判断。往往开发者把注意力都放在对对象值的匹配上,而忽略了类型和完整性这类判断,最终导致整个用例运行失败,但无法准确定位出错的位置。上面提到的对 subviews 是否为空的判断也是这样的一个例子。所以在自定义的匹配规则中就需要考虑到这方面的问题,如代码 15 的 matches: 方法中,先要对传入的泛型对象 item 校验是否为字符串,后再转化为 CGPoint 对象,并进行相应比对。示例中给出的是一种较简单的情况,在更复杂的情况下,除了对泛型对象的类进行校验,还要校验其是否响应某方法,属性类型,空判断,等。

复制代码
#import "HCIsEqualToPoint.h"
#import <OCHamcrestIOS/HCDescription.h>
id <HCMatcher> HC_equalToPoint(CGPoint point)
{
return [HCIsEqualToPoint equalToPoint:point];
}
@implementation HCIsEqualToPoint
+ (id)equalToPoint:(CGPoint)point
{
return [[self alloc] initWithPoint:point];
}
- (id)initWithPoint:(CGPoint)point
{
self = [super init];
if (self) {
self.x = point.x;
self.y = point.y;
}
return self;
}
- (BOOL)matches:(id)item
{
if (! [item isKindOfClass:[NSString class]]) {
return NO;
}
CGPoint point = CGPointFromString((NSString *)item);
return (point.x == self.x && point.y == self.y);
}
- (void)describeTo:(id<HCDescription>)description
{
[description appendText:@"Point not equaled."];
}
@end

代码 15,扩展匹配规则 HC_equalToPoint 实现

一个操作多个测试方法

以上提到的几个例子中所测试的都是非常简单的操作,所以一个测试方法覆盖了一个或多个操作,但对于较复杂的操作,往往需要多个测试方法,循序渐进的断言。例如测试通过 URL 生成 UMViewController 的用例,生成一个 UMViewController 实例由简单到复杂可以有三种简单方式:简单的 URL 生成,带参数的 URL 生成和带 Query 字典的 URL 生成,此外还有 URL 参数和 Query 字典共用的方式。所以对于这个操作至少需要使用 4 个测试方法(代码 16)分别进行测试。

复制代码
(void)testViewControllerForSimpleURL
{
self.viewControllerA = (ViewControllerA *)[self.navigator
viewControllerForURL:
[NSURL URLWithString:@"um://viewa"]
withQuery:nil];
HC_assertThat(self.viewControllerA, HC_instanceOf([UMViewController class]));
HC_assertThat(self.viewControllerA, HC_isA([ViewControllerA class]));
}
- (void)testViewControllerForURLWithArgs
{
self.viewControllerA = (ViewControllerA *)[self.navigator
viewControllerForURL:[NSURL URLWithString:@"um://viewa?<br></br>p1=v1&p2=v2"]
withQuery:nil];
HC_assertThat(self.viewControllerA, HC_instanceOf([UMViewController class]));
HC_assertThat(self.viewControllerA, HC_isA([ViewControllerA class]));
HC_assertThat([self.viewControllerA.params allKeys], HC_containsInAnyOrder<br></br>(@"p1", @"p2", nil));
GHAssertEqualStrings(self.viewControllerA.params[@"p1"], @"v1", @"param error.");
GHAssertEqualStrings(self.viewControllerA.params[@"p2"], @"v2", @"param error.");
}
- (void)testViewControllerWithQuery
{
self.viewControllerA = (ViewControllerA *)[self.navigator
viewControllerForURL:
[NSURL URLWithString:@"um://viewa"]
withQuery:@{@"k1":@"v1", @"k2":@"v2"}];
HC_assertThat([self.viewControllerA.query allKeys], HC_containsInAnyOrder<br></br>(@"k1", @"k2", nil));
GHAssertEqualStrings(self.viewControllerA.query[@"k1"], @"v1", @"param error.");
GHAssertEqualStrings(self.viewControllerA.query[@"k2"], @"v2", @"param error.");
}
- (void)testViewControllerForURLAndQuery
{
self.viewControllerA = (ViewControllerA *)[self.navigator
viewControllerForURL:
[NSURL URLWithString:@"um://viewa?p1=v1&p2=v2"]
withQuery:@{@"k1":@"v1", @"k2":@"v2"}];
HC_assertThat([self.viewControllerA.params allKeys], HC_containsInAnyOrder<br></br>(@"p1", @"p2", nil));
GHAssertEqualStrings(self.viewControllerA.params[@"p1"], @"v1", @"param error.");
GHAssertEqualStrings(self.viewControllerA.params[@"p2"], @"v2", @"param error.");
HC_assertThat([self.viewControllerA.query allKeys], HC_containsInAnyOrder<br></br>(@"k1", @"k2", nil));
GHAssertEqualStrings(self.viewControllerA.query[@"k1"], @"v1", @"param error.");
GHAssertEqualStrings(self.viewControllerA.query[@"k2"], @"v2", @"param error.");
}

代码 16,测试通过 URL 生成 UMViewController 的用例

一个测试方法多次断言

除了一个操作需要多个测试方法的情况,在同一个测试方法中也会有对一个结果进行多次断言的情况(上述用例代码 16 中已经是这种情况,一下用例更具代表性)。这种情况发生在操作结果较为复杂的情况下,例如生成一个 UMNavigationController(代码 17)就是这种情况:UMNavigationController 的初始化方法是带 RootViewController 参数的,所以初始化的实例除了判断其本身是否为 UINavigationController 的子类和 UMNavigationController 实例外,还要判断 rootViewController 的合法性,以及 viewControllers 数组的正确性。

复制代码
(void)testInitWihtRootViewControllerURL
{
UMNavigationController *navigator = [[UMNavigationController alloc]
initWithRootViewControllerURL:[NSURL URLWithString:@"um://viewb"]];
HC_assertThat(navigator, HC_instanceOf([UINavigationController class]));
HC_assertThat(navigator, HC_isA([UMNavigationController class]));
HC_assertThat(navigator.rootViewController,
HC_instanceOf([UMViewController class]));
HC_assertThat(navigator.rootViewController, HC_isA([ViewControllerB class]));
HC_assertThatInteger(navigator.viewControllers.count, HC_equalToInteger(1));
HC_assertThat(navigator.viewControllers,
HC_hasInAnyOrder(HC_instanceOf([UMViewController class]), nil));
HC_assertThat(navigator.viewControllers,
HC_hasInAnyOrder(HC_isA([ViewControllerB class]), nil));
HC_assertThat(navigator.viewControllers,
HC_hasInAnyOrder(HC_is(navigator.rootViewController), nil));
}

代码 17,测试生成 UMNavigationController 的用例

总结

本文一共取了 URLManager 中的 17 段代码片段作为例子,介绍了从利用测试框架提供的断言方法进行简单的测试,一直到使用自定义匹配引擎规则创建较复杂测试用例,并且提到了部分测试引擎和匹配引擎使用过程中会遇到的陷阱。旨在推动开发者能够在开发过程中更简单高效的使用单元测试,为提升代码质量增加一份保障。读者可以在 URLManager 的工程中阅读更多的测试用例代码。

此前预告单元测试系列文章共 3 篇,由于此前的 WWDC 2013 上新版本的 XCode 在单元测试方面做了比较大的更新,所以我会在两周内再为大家介绍一下有关新版本 XCode 的单元测试和持续集成的相关内容,作为单元测试系列文章的番外篇,谢谢大家。


感谢李永伦对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2013-08-27 06:403645

评论

发布
暂无评论
发现更多内容

Java并发编程:多线程并发内存模型

码农架构

Java并发

架构1期 第十二周作业

haha

架构师训练营 12 周笔记

郎哲158

COMP矿池矿机系统开发案例分析

系统开发咨询1357O98O718

COMP矿池矿机系统开发介绍

架构师训练营 12 周作业

郎哲158

5分钟完成业务实时监控系统搭建,是一种什么样的体验?

阿里巴巴中间件

体验 监控

年终盘点 | 七年零故障支撑双11的消息中间件 RocketMQ,怎么做到的?

阿里巴巴中间件

消息中间件 双十一

如何降低微服务测试成本?我的经验之谈

阿里巴巴中间件

深入浅出理解视频编解码技术

拍乐云Pano

音视频 RTC 拍乐云 视频编解码 视频算法

与前端训练营的日子 --Week07

SamGo

学习

ETH场外交易系统开发流程丨ETH场外交易开发源码案例

系统开发咨询1357O98O718

ETH场外交易系统开发

有道逻辑英语-时态新发现笔记

Leo

学习 大前端 笔记 时态

投行工作的本质 | 读《投行职业进阶指南:从新手到合伙人》

邓瑞恒Ryan

读书笔记 投资 金融 投行 职业第二曲线

构建一张音视频全球大网究竟需要多少个节点?Pano Backbone技术探秘

拍乐云Pano

音视频 RTC 拍乐云

减肥为什么会失败,有可能是因为你仍然在摄入容易消化的食用糖。

叶小鍵

科普 减肥、廋身 盖里·陶比斯 加工食用糖

「奇淫技巧」如何写最少的代码

Kerwin

Java 代码设计 代码技巧

区块链钱包系统开发方案丨多币种钱包系统开发详情

系统开发咨询1357O98O718

区块链钱包开发

LeetCode题解:433. 最小基因变化,BFS,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

TRONex波场智能合约系统开发详解丨TRONex波场链系统开发(源码)

系统开发咨询1357O98O718

系统开发 TRONex波场智能合约 APP开发

智天下APP系统开发|智天下软件开发

系统开发

vivo 全球商城:从 0 到 1 代销业务的融合之路

vivo互联网技术

架构 分布式 商城项目 商城

产品推荐 | 还在自研?快来解锁拍乐云互动白板

拍乐云Pano

音视频 在线教育 RTC 互动白板

喜讯 | 拍乐云荣登2020「年度最具投资价值创新企业TOP20」榜单

拍乐云Pano

音视频 拍乐云

第十二周 架构方法学习总结 —— 数据应用

兵长

第12周总结

饭桶

第12周作业

饭桶

三金本体挖矿模式系统开发丨三金本体平台源码设计

系统开发咨询1357O98O718

三金本体挖矿模式源码

使用Angular8和百度地图api开发《旅游清单》

徐小夕

Java angular.js 大前端 angular

OKO疯矿链系统开发案例(源码)

系统开发咨询1357O98O718

OKO疯矿链系统开发

Forsage系统开发(模式分析)

系统开发咨询1357O98O718

Forsage系统开发案例介绍

BMEX交易所系统软件开发|BMEX交易所APP开发

系统开发

iOS开发中的单元测试(三)——URLManager中的测试用例解析_软件工程_高嘉峻_InfoQ精选文章