【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

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:403640

评论

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

基于文心大模型套件ERNIEKit实现文本匹配算法,模块化方便应用落地

汀丶人工智能

自然语言处理 nlp 2月月更 2月日更 文本匹配算法

探讨丨传统行业必须数字化转型吗?

优秀

数字化转型

活动预告|Triton Meetup 2023

AI Infra

AI

宽表为什么横行?

王磊

Java Map操作解锁新姿势

派大星

音乐APP用户争夺战,火山引擎VeDI助力用户体验升级!

字节跳动数据平台

大数据 增长 音乐 企业号 2 月 PK 榜

线程私有变量ThreadLocal详解

Java随想录

Java 线程 并发

中国工商银行签约易观千帆,夯实数字基石,助力用户价值增长

易观分析

金融 银行

中美ChatGPT的商业化分野

脑极体

ChatGPT

一文盘点,ZBC的应用场景与通缩场景

鳄鱼视界

智能汽车商业化、产业化演进及投资机会分析

不脱发的程序猿

汽车电子 智能汽车商业化 汽车行业投资机会分析

OKR之剑·实战篇06:OKR致胜法宝-氛围&业绩双轮驱动(下)

vivo互联网技术

团队管理 OKR

2023年第一季度汽车行业行情预测分析

不脱发的程序猿

汽车电子 2023年第一季汽车行业分析

用这4招优雅的实现Spring Boot 异步线程间数据传递

小小怪下士

Java spring 程序员 springboot

微服务 SpringBoot 整合 Redis GEO 实现附近商户功能

做梦都在改BUG

Java redis 微服务 Spring Boot

银行业上云进行时,OLAP 云服务如何解决传统数仓之痛?

Kyligence

OLAP技术 传统数仓

进击中的 Zebec 生态,Web2 与 Web3 世界的连接器

BlockChain先知

2022Q4手机银行运营亮点:“新版本迭代潮”叠加“个人养老金账户争夺战”

易观分析

金融 银行 经济

瑞萨RH850 CS+环境下设置堆和栈空间

不脱发的程序猿

嵌入式 汽车电子 MCU RH850 瑞萨IDE

Three.js 进阶之旅:物理效果-碰撞和声音 💥

dragonir

CSS JavaScript html 前端 three.js

用户卖家平台三方螺旋成长 如何让商品推荐更智能

阿里技术

全球化技术能力

OneFlow源码解析:Eager模式下的SBP Signature推导

OneFlow

人工智能 深度学习 框架解析

【Redis 故障排查】「连接失败问题排查和解决」带你总体分析CPU及内存的使用率高问题排查指南及方案

洛神灬殇

redis 性能调优 缓存服务 2月日更

Java 发展史

kcodez

Java 后端

入门数据分析师的最强秘籍,都在这4本书里!

博文视点Broadview

5 如何优雅的告诉老板复制 ChatGPT几乎是“impossible”的?

涛哥 数字产品和业务架构

ChatGPT 业务架构师

ChatGPT时代的打工人众生相

白洞计划

ChatGPT

2023-02-14:魔物了占领若干据点,这些据点被若干条道路相连接, roads[i] = [x, y] 表示编号 x、y 的两个据点通过一条道路连接。 现在勇者要将按照以下原则将这些据点逐一夺回:

福大大架构师每日一题

算法 rust 福大大

ChatGPT入门案例|商务智能对话客服(一)| 社区征文

TiAmo

AI ChatGPT

实现一个简单的Database10(译文)

GreatSQL

sqlite myslq greatsql greatsql社区

面试官:如果 MySQL 数据库中的数据丢失,有哪些补救的办法呢?

做梦都在改BUG

Java MySQL 数据库

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