在命令行中舞蹈

在我们的日常开发工作中,通常有一部分重复性的工作。这些工作一般复杂度很低,对认知要求不高,属于浮浅工作,存在很大的自动化空间,因此最适合在命令行中完成。将这类工作尽量简化,我们可以更专注的投入到更有价值的深度工作中,提升工作效率。

本文总结了帮助我节省了无数时间的一些经验,希望对你也有帮助。

目录定位

在终端中,定位到指定目录通常需要数次的 cdls,繁琐的操作让每一个想打开终端的人却步。

我们需要一个工具在目录之间快速跳转,而 fasd 正是这样的工具。fasd 帮助我们直接跳转切换过的目录。

$ j some_directory

对于一些已经在 Finder 中打开了目录的场景,可以使用 go2Shell 在命令行直接切换到当前目录。

在使用 fasd 的初期,它通常能百分百命中我们预期的目录。然而经过一段使用之后,这段时间内切换过的目录可能会污染 fasd 的预测结果。

简单查阅 fasd 的文档,可以知道它的数据存储在 ~/.fasd 文件中,每一行存储了目录的相关信息:

/Users/kukushi/Documents/Project|10.7195|1547211464

| 划分了目录,访问频率,上一次访问时间。利用这些信息,我们可以对 .fasd 进行一些清理。于是乎,我编写了一个脚本执行一些简单的清理,让跳转更加的精确。

将以下代码 .zshrc / .bashrc 下,终端每次启动时都会执行此脚本。

# 需要将这个路径改为你存放这个脚本的路径
cleanFASDScript=~/Documents/Scripts/script/clean_fasd.py
if [ -f "$cleanFASDScript" ]; then
  python3 "$cleanFASDScript" -s true
fi

打开工程

进入到工作目录后,我们需要打开工程文件。 对于 Xcode 工程,它提供了 xed 命令让开发者从命令行打开工程文件。

xed

Opens files for editing in XCode.

- Open file in XCode:
    xed file1

- Open file(s) in XCode, create if it doesn't exist:
    xed -c filename1

- Open a file in XCode and jump to line number 75:
    xed -l 75 filename

但是,在一些 Edge Cases,如目录下没有工程文件,xed 会尝试 Markdown 或其他文件,这通常不是我们需要的。

既然 Xcode 提供的不好用,不如让我们自己来做一个吧。编写简单的 bash 代码,利用简单的正则匹配,我们可以找到需要打开的工程文件。

oop() {
    file=$(find -E . -regex ".*xcworkspace" -maxdepth 1)
	fileLength=${#file}
	if [ "$fileLength" = 0 ]; then
		file=$(find -E . -regex ".*xcodeproj" -maxdepth 1)
	fi
	fileLength=${#file}

	if [ "$fileLength" = 0 ]; then
		echo ">>> 🤔  No Xcode Project Found!"
	else
		echo ">>> 💪  Opening $file"
		xcode=$(xcode-select -p)
		xcode=$(echo $xcode | cut -d'/' -f-3)
		open -a $xcode $file
	fi

习惯使用 AppCode 的话,可以把 Bash 代码中的 Xcode 替换为 AppCode。

同样的这份代码需要放到 .zshrc / .bashrc 下,改动之后需要 source .zshrc/.bashrc 让改动生效。

对于非 Xcode 工程,我习惯用 VSCode 编辑,它是如此常用,以至于我也为它设了一个别名:

alias c='code .'

提交代码

在每一个使用类 git-flow 开发流程的团队中,同步 dev 代码几乎是每日的例行公事。我们可以简单的将一些命令封装成一个 function:

syncdev() {
	git stash

	echo ">>> Updating dev"
	git checkout dev
	git pull origin dev

	echo ">>> Applying diff"
	git co -
	git rebase dev

	git stash pop
}

更快的 “Gitlab”

使用 lab,我们可以在命令行直接完成一些常规的 Gitlab 操作。

使用 brew 可以安装 lab:

$ brew install zaquestion/tap/lab

第一次使用 lab,需要输入一些 gitlab 的信息。

创建 MR

使用 lab,我们可以直接创建 MR:

$ lab mr create origin $branch -a $assigne -m $message1 -m $mesasge2

对于大部分的提交,MR 的第一行 Message (即 Title)可以简化为最近一次的 Commit Message。而对于有 Code Review 合码机制的团队,message 的第二行通常是需要进行 Review 的同事,因此我们的命令可以简化成:

# mmr $branch $reviewer
mmr () {
	latestMessage=$(git log -1 --pretty=%B)
	lab mr create origin $1 -a $2 -m $latestMessage -m $2
}

经过这轮简化,创建一个 MR 只需:

mmr dev @someone

你会爱上这种过感觉!

dotfiles

当习惯在命令行进行各种操作之后,我们会沉淀下很多有用的函数/别名/配置,这通常是开始构建自己的 dotfiles 的时候了,具体可以参考 dotfiles。dotfiles 帮助我们管理这些文件,作为一个 bouns point,我们可以在多个设备上”同步” dotfiles 了。

总结

本文总结了我在日常开发中使用命令行的一些经验,希望对其他同学有所帮助,也欢迎同学们交流。

每种工具都有长短,命令行长在执行一些操作,而 GUI 长在界面查看(如对比 Diff),没有必要局限于某种工具。重要是的 Picking the right tool for the job。

Tags: Shell, productivity   |   Fork on GitHub

iOS UI Testing 不完全踩坑指南

最近研究了一下 UI Testing 的使用,发现还是有蛮多坑的,本文会介绍笔者遇到的坑和解决方案。本文不会涉及到如何编写一个 Test Case,因此希望你已经大致了解 UI Testing (可以看 onevcat 大大的介绍) 和 UI Testing Cheat Sheet.

与宿主应用 (Host App) 交互

UI Testing 运行在另外一个进程中,因此直接无法访问宿主应用的信息。那当我们希望在特定的启动条件下测试应用,要如何操作呢? 答案是利用 XCUIApplicationlaunchArguments

测试进程还是可以通过 Inter-process communication 与宿主进程通信,XCUIElement 的查询就是这么实现的。

首先,需要在 Test 中设定参数:

let app = XCUIApplication()
app.launchArguments = ["ResetDefaults"]
app.launch()

其次,在宿主应用中添加处理这些参数的代码:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    
    #if DEBUG
    var arguments = ProcessInfo.processInfo.arguments
    arguments.removeFirst()
    setupTestingEnvironment(with: arguments)
    #endif
    
    configureSideMenu()
    return true
}

当然,这种做法的坏处是测试代码侵入了应用代码。虽然可以通过使用宏让这些代码在 Release 状态下不生效,但还是无法保持代码的整洁。

更优雅的 XCUIElement 获取

在使用自动录制生成测试代码时,经常会产生令人非常费解的获取 XCUIElement 的代码。这种代码基本无法理解,更谈不上维护了。

let validationPopup = app.children(matching: .window).element(boundBy: 0)
.children(matching: .other).element(boundBy: 1).children(matching: .other).element(boundBy: 1)

如何改善这种情况呢?让我们回忆一下,UI Testing 是构建与 Accessibility 之上的,Accessibility 提供了一个 accessibilityIdentifier 属性,利用它,可以帮助我们减少自动生成的代码。

An identifier can be used to uniquely identify an element in the scripts you write using the UI Automation interfaces. Using an identifier allows you to avoid inappropriately setting or accessing an element’s accessibility label.

class CustomView: UIView {
    // ...
}

let view = CustomView()
view.accessibilityIdentifier = "CustomView"

// Testsing
let customView = app.otherElements["CustomView"]

获取调试信息

debugDescription 可以输出当前视图的层级结构,查看更多的信息。

let element = /* can be element or app */
print(element.debugDescription)

强制点击

有时候,系统会错误的认为按钮是不可点击的(如在 TableView 中的按钮),这时你尝试通过 tap() 点击按钮会触发 Unable to find hit point。这时可以使用 XCUICoordinate 签证强制点击:

/// force taps a view if it reports to be not hittable - useful for buttons in cells
func forceTap() {
    if isHittable {
        self.tap()
    } else {
        // You can also try (0, 0)
        let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
        coordinate.tap()
    }
}

切换输入框

这也是个很奇怪的问题。Testing 在多个输入框切换焦点是会莫名的失败,如:

let userNameTextField = app.textFields["username"]
userNameTextField.tap()
userNameTextField.typeText(userName)

let passwordField = app.textFields["password"]
passwordField.tap() // Error!!!
passwordField.typeText(userName)

passwordField.tap() 可以无法正确的执行。一个 workaround 在第一个输入框输入完成之后,将键盘弹下再弹出,然后尝试输入:

/// hides keyboard if present & obstructs hit space
func hideKeyboardIfNeeded() {
    if keyboardHideButton.coordinate(withNormalizedOffset: CGVector.zero).screenPoint.x < UIScreen.main.bounds.width {
        keyboardHideButton.tap()
    }
}

Tags: UITesting, iOS   |   Fork on GitHub