QA seven's blog


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

小QA学习前端系列之vue Class 与 Style 绑定

发表于 2017-10-20

小QA学习前端系列之vue Class 与 Style 绑定

绑定 HTML Class

对象语法
使用 v-bind:class

1
<div v-bind:class="{ active: isActive }"></div>

当然也可以绑定一个 data对象

1
2
3
4
5
6
7
8
<div v-bind:class="classObject"></div>
data: {
classObject: {
active: true,
'text-danger': false
}
}

也可以绑定一个返回对象的计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div v-bind:class="classObject"></div>
data: {
isActive: true,
error: null
},
computed: {
classObject: function () {
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}

数组语法
我们可以把一个数组传给 v-bind:class,以应用一个 class 列表:

1
2
3
4
5
6
7
<div v-bind:class="[activeClass, errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}

用在组件上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
例如,如果你声明了这个组件:
Vue.component('my-component', {
template: '<p class="foo bar">Hi</p>'
})
然后在使用它的时候添加一些 class:
<my-component class="baz boo"></my-component>
HTML 将被渲染为:
<p class="foo bar baz boo">Hi</p>
对于带数据绑定 class 也同样适用:
<my-component v-bind:class="{ active: isActive }"></my-component>
当 isActive 为真值 (译者注:truthy 不是 true,参考这里) 时,HTML 将被渲染成为:
<p class="foo bar active">Hi</p>

绑定内联样式

对象语法

v-bind:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用单引号括起来) 来命名:

1
2
3
4
5
6
7
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
activeColor: 'red',
fontSize: 30
}

绑定到一个样式对象

1
2
3
4
5
6
7
8
<div v-bind:style="styleObject"></div>
data: {
styleObject: {
color: 'red',
fontSize: '13px'
}
}

数组语法

v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上:

1
<div v-bind:style="[baseStyles, overridingStyles]"></div>

自动添加前缀

当 v-bind:style 使用需要添加浏览器引擎前缀的 CSS 属性时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

多重值

从 2.3.0 起你可以为 style 绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:

这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex。

小QA学习前端系列之vue计算属性和观察者

发表于 2017-10-19

小QA学习前端系列之vue计算属性和观察者

对于任何复杂逻辑,你都应当使用计算属性

我可以在模板放入逻辑

1
2
3
<div id="example">
{{ message.split('').reverse().join('') }}
</div>

也可以用$watch来计算逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="demo">{{fullName}}</div>
var vm = new Vue({
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
}
})
vm.$watch('firstName', function (val) {
this.fullName = val + ' ' + this.lastName
})
vm.$watch('lastName', function (val) {
this.fullName = this.firstName + ' ' + val
})

通常情况下,使用计算属性会比使用过程式的$watch回调更合适

1
2
3
4
5
6
7
8
9
10
11
12
var vm = new Vue({
el:'#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})

是不是代码量少多了,开心吧
但要记住计算属性是基于它们的依赖进行缓存的,而直接在vue的method中每当触发重新渲染时,调用方法将总会再次执行函数。
如果你不想使用缓存 也可以 只需要cache关闭开关就好

1
2
3
4
5
6
7
8
computed: {
example: {
cache: false,
get: function () {
return Date.now() + this.msg
}
}
}

计算属性的 setter

计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
// ...

现在再运行 vm.fullName = ‘John Doe’ 时,setter 会被调用,vm.firstName 和 vm.lastName 也会相应地被更新。

侦听器

那什么时候用watch
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<div id="watch-example">
<p>
Ask a yes/no question:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
<!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
el: '#watch-example',
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.getAnswer()
}
},
methods: {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
getAnswer: _.debounce(
function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
},
// 这是我们为判定用户停止输入等待的毫秒数
500
)
}
})
</script>

结果:

Ask a yes/no question:

I cannot give you an answer until you ask a question!

在这个示例中,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

除了 watch 选项之外,您还可以使用命令式的 vm.$watch API。

小QA学习前端系列之vue模板

发表于 2017-10-18

小QA学习前端系列之vue模板

之前我们已经学习vue实例,已经理解如何初始化一个vue实例,以及实例中数据与方法使用,还了解生命周期,那接下来我要学习vue模板

插值

文本

使用“Mustache”语法 (双大括号) 的文本插值

1
<span>Message: {{ msg }}</span>

原始 HTML

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML ,你需要使用 v-html 指令

1
<span>Message: {{ msg }}</span>

这个标签可能导致xss攻击

特性

Mustache 语法不能作用在 HTML 特性上,遇到这种情况应该使用 v-bind 指令
例如 id class 等

1
<div v-bind:id="TEST"></div>

可以 省略前面的v-bind 直接使用:id

使用 JavaScript 表达式

用在v-bind中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="app">
<span class="box" :style="[isA?styleA:'', styleB]">我是字</span>
</div>
<script>
new Vue({
el: "#app",
data:{
styleA:{
fontSize: '30px',
color: 'red'
},
styleB:{
textShadow: '5px 2px 6px #000'
},
isA: false
}
})
</script>

用在Mustache中

1
{{ ok ? 'YES' : 'NO' }}

有个限制就是,每个绑定都只能包含单个表达式

指令

指令 (Directives) 是带有 v- 前缀的特殊属性。
v-text

预期:string

详细:

更新元素的 textContent。如果要更新部分的 textContent ,需要使用 {{ Mustache }} 插值。

示例:
1
2
3
4
<span v-text="msg"></span>
<!-- 和下面的一样 -->
<span>{{msg}}</span>

v-show

预期:any

用法:

根据表达式之真假值,切换元素的 display CSS 属性。

当条件变化时该指令触发过渡效果。

v-if

预期:any

用法:

根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。如果元素是 <template> ,将提出它的内容作为条件块。

当条件变化时该指令触发过渡效果。

参数

一些指令能够接收一个“参数”,在指令名称之后以冒号表示。

在这里 href 是参数,告知 v-bind 指令将该元素的 href 属性与表达式 url 的值绑定。
另一个例子是 v-on 指令,它用于监听 DOM 事件:

在这里参数是监听的事件名。

修饰符

修饰符 (Modifiers) 是以半角句号 . 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault():

缩写

v- 前缀作为一种视觉提示,用来识别模板中 Vue 特定的特性。
v-bind 缩写

1
2
3
4
<!-- 完整语法 -->
<a v-bind:href="url"></a>
<!-- 缩写 -->
<a :href="url"></a>

v-on 缩写

1
2
3
4
5
<!-- 完整语法 -->
<a v-on:click="doSomething"></a>
<!-- 缩写 -->
<a @click="doSomething"></a>

ok 今天主要介绍基本的模板语法,其实还有一些语法没有解释,根据官方文档咱们一步一步来

小QA学习前端系列之vue实例

发表于 2017-10-17

小QA学习前端系列之vue实例

不管是学习任何语言和框架,或者工具,都应该先把官方文档略读一遍,之后找个demo项目,开始看代码,然后照着人家的demo 敲一遍,基本上就入门,我是笨人,这是我认为学习效率最高的方式。
API:https://cn.vuejs.org/v2/api/#v-bind

啥是vue

就是一个前端框架,他吸收了react的优点例如:

1. 使用 Virtual DOM
2. 提供了响应式 (Reactive) 和组件化 (Composable) 的视图组件。
3. 将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。

同时它也融合了AngularJS 的一些优点

1. 数据绑定
2. 指令与组件

还有其他的一些优秀框架例如konckout等
详细内容大家可以看
https://cn.vuejs.org/v2/guide/comparison.html

生命周期

https://cn.vuejs.org/images/lifecycle.png
现在的软件设计都利用了生命周期概念,帮助在开发者在开发中更好的处理数据,例如Android的生命周期,React的生命周期。
VUE大概的流程如下

1
2
3
4
beforeCreateed()-> created()->beforeMount()->mounted(){->beforeUpdate()
->updated()}->beforeDestroy()->destroyed()
我们先大致了解下 vue 的生命周期,这对于以后我们看别人的代码有很大好处

开始实践

vue实例

创建vue实例

1
2
3
var vm = new Vue({
// 选项
})

数据与方法

data 对象中能找到的所有的属性。
值得注意的是只有当实例被创建时 data 中存在的属性是响应式的。
所以后面添加的属性 都无效
Vue 实例暴露了一些有用的实例属性与方法。都有前缀$

1
2
3
4
5
6
7
8
9
10
11
var data = { a: 1 }
var vm = new Vue({
el: '#example',
data: data
})
vm.$data === data // => true
vm.$el === document.getElementById('example') // => true
// $watch 是一个实例方法
vm.$watch('a', function (newValue, oldValue) {
// 这个回调将在 `vm.a` 改变后调用
})

具体的API如下https://cn.vuejs.org/v2/api/#实例属性
因为我已经熟悉了node与webpack,所以直接上vue-cli 初始化项目
不熟悉的同学 还是建议在
https://jsfiddle.net/chrisvfritz/50wL7mdz/
这个模板下学习
OK 第一节完毕 明天继续按着文档看模板语法

小QA学习前端系列之练习实践vuex-shopping-cart

发表于 2017-10-15

小QA学习前端系列之练习实践vuex-shopping-cart

代码地址

https://github.com/vuejs/vuex/tree/dev/examples/shopping-cart

直接 npm install 或者yarn ,安装依赖

之后,npm run dev 运行

页面如下

shop

废话不多说,直接看源码。

源码解析

先来看看文件目录

文件

该demo包含了2个module,一个叫cart,另一个叫product.

先看cart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#shop是获取数据的api
import shop from '../../api/shop'
#引入了 mutation-types 使用常量替代 Mutation 事件类型
import * as types from '../mutation-types'
// initial state 初始化了2个状态也可叫数据对象,added:
// shape: [{ id, quantity }]
const state = {
added: [],
checkoutStatus: null
}
// getters getters过滤条件必须是bollean值.根据bolean的条件返回具体数据对象。null在JavaScript中if条件下 均被视为false
const getters = {
checkoutStatus: state => state.checkoutStatus
}
// actions 在actions中提交mutation,并且可以包含任何的异步操作。actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据(但是还是通过mutation来操作,因为只有它能操作
const actions = {
checkout ({ commit, state }, products) {
//解构 state.added对象里面的值,并将至存放到savedCartItems对象中
const savedCartItems = [...state.added]
commit(types.CHECKOUT_REQUEST)
shop.buyProducts(
products,
() => commit(types.CHECKOUT_SUCCESS),
() => commit(types.CHECKOUT_FAILURE, { savedCartItems })
)
}
}
// mutations 在Vuex中store数据改变的唯一方法就是mutation
<!--
mutations,里面装着一些改变数据方法的集合,这是Veux设计很重要的一点,就是把处理数据逻辑方法全部放在mutations里面,使得数据和视图分离。 -->
const mutations = {
[types.ADD_TO_CART] (state, { id }) {
state.lastCheckout = null
const record = state.added.find(p => p.id === id)
if (!record) {
state.added.push({
id,
quantity: 1
})
} else {
record.quantity++
}
},
[types.CHECKOUT_REQUEST] (state) {
// clear cart
state.added = []
state.checkoutStatus = null
},
[types.CHECKOUT_SUCCESS] (state) {
state.checkoutStatus = 'successful'
},
[types.CHECKOUT_FAILURE] (state, { savedCartItems }) {
// rollback to the cart saved before sending the request
state.added = savedCartItems
state.checkoutStatus = 'failed'
}
}
export default {
state,
getters,
actions,
mutations
}

在来看看 product

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import shop from '../../api/shop'
import * as types from '../mutation-types'
// initial state
const state = {
all: []
}
// getters JS中数组(不管是不是空的)属于对象类型,对象类型转为boolean类型(内部对应ToBoolean()操作)时都是true。
const getters = {
allProducts: state => state.all
}
// actions
const actions = {
getAllProducts ({ commit }) {
shop.getProducts(products => {
commit(types.RECEIVE_PRODUCTS, { products })
})
}
}
// mutations
const mutations = {
[types.RECEIVE_PRODUCTS] (state, { products }) {
state.all = products
},
[types.ADD_TO_CART] (state, { id }) {
state.all.find(p => p.id === id).inventory--
}
}
export default {
state,
getters,
actions,
mutations
}

看完了 module,再来看看 action

1
2
3
4
5
6
7
8
9
10
//这个是总得action 每个module 都可以有自己的action
import * as types from './mutation-types'
//很简单 判断存货是否大于0 然后提交给mutation具体id
export const addToCart = ({ commit }, product) => {
if (product.inventory > 0) {
commit(types.ADD_TO_CART, {
id: product.id
})
}
}

继续 再来看看总得getter

1
2
3
4
5
6
7
8
9
10
11
//查询 返回对象
export const cartProducts = state => {
return state.cart.added.map(({ id, quantity }) => {
const product = state.products.all.find(p => p.id === id)
return {
title: product.title,
price: product.price,
quantity
}
})
}

再来看定义所有mutation类型文件mutation-types.js

1
2
3
4
5
export const ADD_TO_CART = 'ADD_TO_CART'
export const CHECKOUT_REQUEST = 'CHECKOUT_REQUEST'
export const CHECKOUT_SUCCESS = 'CHECKOUT_SUCCESS'
export const CHECKOUT_FAILURE = 'CHECKOUT_FAILURE'
export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'

最后index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import cart from './modules/cart'
import products from './modules/products'
import createLogger from '../../../src/plugins/logger'
Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
actions,
getters,
modules: {
cart,
products
},
strict: debug,
plugins: debug ? [createLogger()] : []
})

至此 所有vuex介绍完毕,然后让我们看这些vuex怎么应用的component中

先来到cart组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<div class="cart">
<h2>Your Cart</h2>
<p v-show="!products.length"><i>Please add some products to cart.</i></p>
<ul>
<li v-for="p in products">
{{ p.title }} - {{ p.price | currency }} x {{ p.quantity }}
</li>
</ul>
<p>Total: {{ total | currency }}</p>
<p><button :disabled="!products.length" @click="checkout(products)">Checkout</button></p>
<p v-show="checkoutStatus">Checkout {{ checkoutStatus }}.</p>
</div>
</template>
<script>
//通过mapgetters方法解构数据
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters({
products: 'cartProducts',
checkoutStatus: 'checkoutStatus'
}),
total () {
return this.products.reduce((total, p) => {
return total + p.price * p.quantity
}, 0)
}
},
methods: {
//分发action
checkout (products) {
this.$store.dispatch('checkout', products)
}
}
}
</script>

在来看看 product组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<ul>
<li v-for="p in products">
{{ p.title }} - {{ p.price | currency }}
<br>
<button
:disabled="!p.inventory"
@click="addToCart(p)">
Add to cart
</button>
</li>
</ul>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: mapGetters({
products: 'allProducts'
}),
methods: mapActions([
'addToCart'
]),
created () {
this.$store.dispatch('getAllProducts')
}
}
</script>

最后将这两个component 引入到app.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div id="app">
<h1>Shopping Cart Example</h1>
<hr>
<h2>Products</h2>
<product-list></product-list>
<hr>
<cart></cart>
</div>
</template>
<script>
import ProductList from './ProductList.vue'
import Cart from './Cart.vue'
export default {
components: { ProductList, Cart }
}
</script>

基本上这个demo就介绍完毕

下来我通过vue-cli初始化一个新项目,将上面所有的代码进行练习 谢谢大家欣赏。
代码已经上传
https://github.com/qileilove/vuex-practice

小QA学习前端系列之vuex

发表于 2017-10-12

一个QA如何学习vuex

什么vuex

其实之前也学习过redux,虽然redux已经把我看得云里雾里的,里面的educer 应该如何拆分、action 应该怎么定义、dispatch 异步怎么做、Ajax 怎么使用、middleware 什么时候需要用、enhancer 干什么的、高阶函数怎么这么多 等等一系列问题,就算是看懂了,放上半年又全还给度娘了,相反vuex却让我记忆犹新,vuex的api 我花半天时间,就已经理解的很透彻了,跟着getting started,已经可以把demo中todo-list搞定了。说了这么多,那么vuex到底是什么,vuex和redux一样都是用来管理状态的,只不过vuex只能用在vue项目中,redux则没有限制,如果你想在react项目使用类似vuex的框架,最近有一个MobX的框架可以值得一试,虽然我也没看,但是大神推荐了,应该没错。

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

以上是官方的解释,很清晰明了,想更进一步的话,我们需要知道vuex是基于flux架构,同时也吸收redux的一些优点,而redux又是基于flux的改进:

把store和Dispatcher合并,结构更加简单清晰
新增state角色,代表每个时间点store对应的值,对状态的管理更加明确

Redux数据流的顺序是:

View调用store.dispatch发起Action->store接受Action(action传入reducer函数,reducer函数返回一个新的state)->通知store.subscribe订阅的重新渲染函数

Flux数据流的顺序是:

View发起Action->Action传递到Dispatcher->Dispatcher将通知Store->Store的状态改变通知View进行改变
而Vuex是专门为Vue设计的状态管理框架,
同样基于Flux架构,并吸收了Redux的优点

Vuex相对于Redux的不同点有:

改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,
无需switch,只需在对应的mutation函数里改变state值即可

由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可

Vuex数据流的顺序是:

View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)

vuex的核心概念

Vuex 中 Store 的模板化定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
},
actions: {
},
mutations: {
},
getters: {
},
modules: {
}
})
export default store
  • State
  • Getter
  • Mutation
  • Action
  • Module

    state

    Vuex就是提供一个仓库,仓库里面放了很多对象。其中state就是数据源存放地,对应于与一般Vue对象里面的data(后面讲到的actions和mutations对应于methods)。
    state: state 定义了应用状态的数据结构,同样可以在这里设置默认的初始状态。

响应书存储:state里面存放的数据是响应式的,Vue组件从store中读取数据,若是store中的数据发生改变,依赖这个数据的组件也会发生更新。(这里“状态”=“数据”),也就是是说数据和视图是同步的。
例如:

1
2
3
4
state: {
projects: [],
userProfile: {}
}

在 Vue 组件中获得 Vuex 状态的几种方法

通用方法:全局的放vuex,局部的放在component的data里

Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)):

1
2
3
4
5
6
7
8
9
10
11
12
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})

通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。让我们更新下 Counter 的实现:

1
2
3
4
5
6
7
8
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}

mapState

mapState的作用是把全局的 state 和 getters 映射到当前组件的 computed 计算属性中,this.$store.state。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import {mapState} from 'vuex'
export default {
computer :
mapState({
count: state => state.count,
// 方法一 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 方法二 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
},
// 映射 this.count 为 store.state.count
'count'
})
}
对象展开运算符
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import actions from './action'
import getters from './getters'
Vue.use(Vuex)
const state = {
userInfo: { phone: 111 }, //用户信息
orderList: [{ orderno: '1111' }], //订单列表
orderDetail: null, //订单产品详情
login: false, //是否登录
}
export default new Vuex.Store({
state,
getters,
actions,
mutations,
})
computed: {
...mapState([
'orderList',
'login'
]),
},
mounted(){
console.log(typeof orderList); ==>undefind
console.log(typeof this.orderList)==>object
}

getter

一句话 getter就是用来进行数据过滤的,然后把过滤后的数据,共享给所有component。

所以getters是store的计算属性

getters过滤条件必须是bollean值.根据bolean的条件返回具体数据对象。

🌰🌰🌰🌰🌰🌰

定义:我们可以在store中定义getters,第一个参数是state

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})

传参:定义的Getters会暴露为store.getters对象,也可以接受其他的getters作为第二个参数;

1
2
3
4
5
6
7
8
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1

调用

1
2
3
4
5
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}

通过mapGetters调用
mapGetters辅助函数仅仅是将store中的getters映射到局部计算属性中,用法和mapState类似

1
2
3
4
5
6
7
8
9
10
11
import { mapGetters } from 'vuex'
computed: {
// 使用对象展开运算符将 getters 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',])}
//给getter属性换名字
mapGetters({
// 映射 this.doneCount 为 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})

Mutation

在Vuex中store数据改变的唯一方法就是mutation

mutations,里面装着一些改变数据方法的集合,这是Veux设计很重要的一点,就是把处理数据逻辑方法全部放在mutations里面,使得数据和视图分离。

重要的原则就是要记住 mutation 必须是同步函数

如何使用

  • mutation结构

每一个mutation都有一个字符串类型的事件类型(type)和回调函数(handler),也可以理解为{type:handler()},这和订阅发布有点类似。先注册事件,当触发响应类型的时候调用handker(),调用type的时候需要用到store.commit方法。

1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) { //注册事件,type:increment,handler第一个参数是state;
// 变更状态
state.count++}}})
store.commit('increment') //调用type,触发handler(state)
  • 载荷(payload)

简单的理解就是往handler(stage)中传参handler(stage,pryload);一般是个对象。

1
2
3
4
mutations: {
increment (state, n) {
state.count += n}}
store.commit('increment', 10)

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

1
2
3
4
5
6
7
8
9
10
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
  • 对象风格的提交方式

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

1
2
3
4
store.commit({
type: 'increment',
amount: 10
})

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:

1
2
3
4
5
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
  • Mutation 需遵守 Vue 的响应规则

最好提前在你的 store 中初始化好所有所需属性。

当需要在对象上添加新属性时,你应该

使用 Vue.set(obj, ‘newProp’, 123), 或者以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:

1
state.obj = { ...state.obj, newProp: 123 }
  • 使用常量替代 Mutation 事件类型

将常量放在单独的文件中,方便协作开发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
  • 在组件中提交 Mutation

提交可以在组件中使用 this.$store.commit(‘xxx’) 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}

action

前面我们讲了mutation中是存放处理数据的方法的集合,我们使用的时候需要commit。但是commit是同步函数,而且只能是同步执行。

所以action出现了。在actions中提交mutation,并且可以包含任何的异步操作。actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据(但是还是通过mutation来操作,因为只有它能操作)

定义actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const store = new Vuex.Store({//创建store实例
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
},
sub(state){
state.count--;
}
},
actions: { //只是提交`commit`了`mutations`里面的方法。
increment (context) {
context.commit('increment')
},
subplus({commit}){
commit('sub');
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。
一般我们会简写成这样
actions: {
increment ({ commit }) {
commit('increment')
},
subplus({commit}){
commit('sub')
}
}

分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

1
2
3
4
5
6
7
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}

Actions 支持同样的载荷方式和对象方式进行分发:

1
2
3
4
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})

// 以对象形式分发

1
2
3
4
store.dispatch({
type: 'incrementAsync',
amount: 10
})

  • 在组件中分发 Action
    你在组件中使用 this.$store.dispatch(‘xxx’) 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import { mapActions } from 'vuex'
    export default {
    // ...
    methods: {
    ...mapActions([
    'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
    // `mapActions` 也支持载荷:
    'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
    add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
    }
    }

Module

背景:在Vue中State使用是单一状态树结构,应该的所有的状态都放在state里面,如果项目比较复杂,那state是一个很大的对象,store对象也将对变得非常大,难于管理。

module:可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}

对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

1
2
3
4
5
6
7
8
9
10
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

1
2
3
4
5
6
7
8
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为命名空间模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})

带命名空间的绑定函数

当使用 mapState, mapGetters, mapActions 和 mapMutations 这些函数来绑定命名空间模块时,写起来可能比较繁琐:

1
2
3
4
5
6
7
8
9
10
11
12
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo',
'some/nested/module/bar'
])
}

对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo',
'bar'
])
}
```而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from ‘vuex’

const { mapState, mapActions } = createNamespacedHelpers(‘some/nested/module’)

export default {
computed: {
// 在 some/nested/module 中查找
…mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 some/nested/module 中查找
…mapActions([
‘foo’,
‘bar’
])
}
}

```

以上便是我2天来学习vuex的心得,大部分参考的是官方的文档,不得不说vuex的文档比redux文档容易理解多,接下来我会把官方的列子购物车玩一下,然后自己在写一个todo-list,vue2 还是大略的过了一遍,还是要写个小项目熟悉框架,一个QA在前端的路途上越走越远,希望学到前端知识能够帮我在测试和自动化的道路上有所突破。

2017年qa应掌握的技能

发表于 2017-04-06

DevOps & 敏捷方法

随着对于在交付期限内完成项目的迫切需求,测试人员需要学习敏捷方法和DevOps,因为它们可以帮助促进团队之间的协作和改进迭代间工作模式。敏捷方法为测试项目提供了项目开发所需要速度与效率,而DevOps可以从开发,分析和质量保证流程中协助跨职能的团队合作,从而在更短的时间内产生出高质量的产品。此外,学习这些方法最终消除了角色间僵化和孤岛,让团队密切关注开发流程和持续发布。
devops
向上面devops工作流一样,devops 打通项目流程每一个环节,从代码的管理,到build、test 到最好部署到prod环境,对于qa来说,了解其中的流程,对于qa工作有非常的好处,通过devops一些监控工具例如airbrake我们可以实施查看系统发生的问题,与dev一起分析问题发生的原因,通过GA newrelic这些工具,我们可以分析系统用户行为,给系统性能测试与分析提供数据上的支持。通过对于了解devops工作流,我们可以分析项目中存在风险,及时改进流程,通过docker等容器化的工具,我们可以将测试放到容器中执行,降低测试对于环境的依赖,进一步改进自动化测试,所以说了解devops知识对于qa来说百利无一害,何乐而不为呢。

自动化
面对应用程序日益增加复杂性和系统集成,依靠手动测试已经无法完成全部测试工作。为了测试浏览器兼容性,性能,无头以及数据库和集成层面,测试人员应该学习自动化相关的技能,因为它可以提供业务逻辑和技术层面更高更准确更可靠支持。此外,还有几种自动化测试工具专门支持特定领域的测试类型,并具有快速高效地完成任务的功能。
selenium
如上图,很多QA或者tester有一个误区,提到自动化张嘴闭嘴都是selenium,甚至有的人连webdriver与selenium区别都不知道,所以这里我要澄清,ui自动化测试只是自动化测试中的一小部分,还包括api,接口,单元测试,集成测试,甚至性能测试我们都应该称为自动化测试。

网络与移动技术
webandmobile

每个测试人员还必须熟悉web和mobile相关技术,以便他们能够更好了解应用程序的类型与架构,如何构建,以及相应的可扩展性,并为以后测试提供相应背景知识。这对于测试人员非常重要,因为它能够指导QA理解项目架构与所面临技术挑战,提供更有效的QA解决方案。
这里学问更多,完全是两大方向,知识领域都非常的广大,例如web端我们可能要测试不同浏览器兼容性,对于mobile来说,尤其是android这种版本碎片化以及各种自定义系统以及ui改更能会造成各种个样的问题,对于mobile我们甚至要考虑更多场景,例如app是否具有内存泄露,电量消耗的快慢,横竖屏的影响等等。不同构建工具,开发环境的区别等对于qa来说又要面对一堆未知领域,没事,继续加油。

SDLC 软件开发生命周期(Systems Development Life Cycle)
sdlc

测试人员也被建议学习软件生命周期管理技能,这将有助于QA更轻松了解应用程序开发任务和测试计划周期。对SDLC周期的深入了解还有助于预测应用程序的复杂性,从而提前采取正确的措施。因此,测试人员还必须学习适用于项目开发生命周期流程的几种开发方法,如瀑布,看板,Scrum,精益等。
作为一个qa,我们应关注软件开发生命周期中的每一步,了解敏捷开发迭代中的各个环节及时作出反馈,所以有时qa也需要承担scrum master的责任,例如某个story进度超过预定期限未完成,我们就应当了解原因,及时与团队成员以及客户沟通。

理性分析与逻辑思维
sdlc
为了保持竞争力,QA还应该学使用理性,分析性和逻辑性思维来思考问题,因为这些技能在测试应用时可以帮助QA识别错误,了解问题的复杂性,评估应用程序的不熟悉的功能并相应地进行测试。具有良好的分析和推理能力有助于根据在不同场景下验证应用程序,并根据预先定义的标准来评估应用程序。这进一步有助于评估相关信息,提出明确的问题,确定优势和弱点,而不会持有偏见,这有助于实施正确的行动方案和解决方案。

社交网络
contact
任何行业的任何专业人士都需要社交网络技能。由于社交网络可以即时访问讨论,资源和内容,因此在这方面的技巧有助于与其它QA进行互动,学习新事物并更新自己的知识体系的广度与深度。拥有社交网络技能还可让您与各种专家进行交流,交流疑问,当然也可以建立长期的合作关系,帮助您的专业技能和企业达到所需目标。

测试工具和技术
tools
每个QA都必须了解不同的测试技术和使用工具。无论你的项目属于那个领域和应用程序类型,都应具有黑盒测试,渗透测试,安全测试,系统测试,单元测试等,测试人员具有多功能性,可以帮助他们在任何类型的项目上工作。此外,随着商业化的专业测试工具数量的增加,例如bug跟踪工具,测试管理工具,GUI测试工具,自动化工具等,测试人员可以了解这些工具的特点,选择适用于所在项目需求工具,以满足不同的需求和复杂性。

编程
tools

当我们谈论编程时,QA当然不会像开发人员一样工作,但是QA需要理解应用程序的实现原理与项目架构,这样我们可以更容易的创建所需的测试。编程知识有助于识别应用程序代码中的可能发生的错误,这就进一步降低了应用程序发生错误的概率。所以建议学习至少两种编程语言,这样的话测试人员就能够更好的了解应用程序的解决方案,以确保的应用程序生命周期的质量。

沟通 - 书面和口头
communicate

每个QA还应具备良好的沟通能力。通过良好的沟通,对于项目的所有角色来说,QA应该是一个好的作家,演讲者,听众和读者,例如将项目的状态更新给客户,向团队通知story的需求变更,与开发人员交流bug的细节,将需求文档转化为测试用例,准备测试报告等。除此之外,良好的沟通有助于表现出个人高度的理解能力,进一步帮助QA在逻辑思维和理性分析基础上向技术人员和非技术人员提供反馈意见。

智力与创意
软件测试并不是一个程序化或平凡的任务,而是一个需要创造力和逻辑分析的过程。智力和创造力无法改变,但是,可以通过质疑应用程序行为并分析应用程序的不同方面来了解其工作原理。另外,通过设计一些应用场景,测试人员可以尝试探索性测试,来识别更多缺陷,并寻求提供有效产品质量的可能解决方案。

测试计划和文档

测试计划和文档对于每个QA至关重要,因为它有助于确定正确的需求以及采取合理的步骤。此外还可以帮助跟踪需求变化,检查测试过程和跟踪偏差,并有助于报告和记录工作。一个记录良好的测试过程也可以帮助个别测试者和企业将正确的预算和资源分配给一个项目,这就是为什么测试计划和文档技能是每个测试人员必须学习的重要技能之一。

项目管理
学习项目管理的技能,帮助QA提高对于处理问题的能力。例如项目中某个环节发生问题,通过项目管理技能,能够及时的分析问题影响广度,及时寻找相关人员协作处理问题,项目管理技能有助于QA了解项目流程中所存在的问题,来帮助更好改进整个测试过程。

客户支持
customers

与过去项目有所不同,现代测试项目要求QA提供客户支持并从他们的角度思考。作为QA并不意味着我只是关心功能是否正确,因为测试项目的成功或失败因素很多,而不仅仅只是局限于功能层面,因此QA应及时应对和支持客户提出的需求,并所他们角度思考哪些方面仍然需要改善。

报告
report
优秀的QA必须拥有良好报告技能,才能向项目的成员与客户提供测试项目和被测试应用的确切状态。这种报告实践可以更好地协调整个测试项目,同时也为高层管理人员提供执行详细的数据,例如测试用例覆盖率,bug的数量,发布时间表等,最终有助于其做出正确的决策。

独立工作
最后,QA应该学习如何独立工作的技能。这将提高他们从需求理解(技术和业务需求)到产出的最终交付工作的能力,采取正确的步骤,在没有他人的帮助或经理的监督。学习独立工作将会增加对他们的信心。

无论经验多少,QA都应努力不断学习和提高软件测试技能和知识。无论是自学习还是参与培训计划,测试人员应不断学习新的方法,以提高测试工作中的效率,并继续应用新技能和学习,使自己处于领先地位。

未命名

发表于 2017-02-16

title: 100% code coverage != 100% test
date: 2017-02-16 10:32:59
tags:

- QA

100% code coverage != 100% test

引言

avatar

很多人看到这个标题时,就在想你都100%代码覆盖了,怎么还会有问题呢?

让我们看看一下代码栗子:

Code example

1
2
3
4
5
6
7
8
9
public class TestCalculator {
public Double add(Double a, Double b) {
return a + b;}
}

再让我们看看用junit写出的测试代码:

@Test

public void testAdd() {

Double a = new Double(1);

Double b = new Double(2);

Double c = new Double(3);

assertEquals(c, testCalculator.add(a, b));

}
当我们在Eclipse使用 EclEmme Code Coverage 插件测试时, 对于这个类我们将得到 100% Line-Coverage.
avatar

一切看起来都那么的完美,真是这样的吗?

好吧,让我们来来看看另一个测试,当其中一个变量为null时,返回值将会怎样?

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testAddNullPointerException() {
Double a = new Double(1);
Double b = null;
Double c = new Double(3);
assertEquals(c, testCalculator.add(a, b));
}

好了,你会发现尽管你的覆盖率为100%,但程序却抛出了NullPointerException异常。

对于很多PM来说会选择第一个测试代码,因为100%的单元测试覆盖率总比50%的测试代码要更好!

但是这有用么?我们可能忽略了更重要的业务场景测试。

哈哈,小小的嘲讽了部分PM。

我们应当转换下思维,思考以下问题。

100%的覆盖率并不意味你的测试代码质量很高,上面那个列子就是典型。

100%的覆盖率并不意味着所有的业务场景都被覆盖。

对于项目我们是否有足够的测试?
avatar

这里的测试不仅仅包括代码级别的单元测试,还要集成测试,接口测试,黑盒测试,可用性测试等等。只有全部得测试通过后,才能说明功能的完整性,仅仅单一的单元测试,对于整个项目来说,基本没用。

所以100%的代码覆盖率值得追求吗?
avatar

是的,这应该是每个程序猿毕生追求的之一,但是如果从项目角度考虑,ROI(投入产出比),如果你的项目是一个短期项目,需要快速上线,那么这时候你需要注重的是测试应覆盖核心功能代码。如果你的项目是一个长期项目,那么高覆盖率是非要有必要的,它意味着可维护性,以及更少的bug。(前提是你的测试采用TDD/BDD方式编写,我见过将测试代码写的一团糟的人,看着他的代码,我宁愿重新一遍)

那么对于一个项目来说覆盖率应该达到多少?

其实没有具体数值能够覆盖到所有的项目,每个项目都应有自己的阈值,但共性是,必须测试必须覆盖主要业务场景,代码的逻辑分支也必须覆盖。

如何改进你的项目代码覆盖率?

阅读和理解项目代码,找出其中需要测试并且与业务强相关的代码。

将之前可读性差测试代码,采用TDD与BDD方式进行重写,提高项目的可维护性与可读性。

代码覆盖率非常重要的意义在于:

分析未覆盖部分的代码,从而反推在前期黑盒测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?是需求/设计不够清晰,还是测试设计的理解有误。

检测出程序中的废代码,可以逆向反推在代码设计中不合理的地方,提醒设计/开发人员理清代码逻辑关系,提升代码质量。

代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量绝对不会高到哪里去,可以作为测试自我审视的重要工具之一。

espresso-api之Matchers探究

发表于 2016-12-25

通过前3篇文章,大家应该对espresso有了大体上的了解,那么今天我们要深入了解它的API,看看espresso的整体架构。
还是通过espresso cheat sheet来进入本次话题。

Espresso备忘单是您在开发过程中可以使用的快速参考。 这个备忘单包含大多数可用的Matchers,ViewActions和ViewAsertions。
让我们先来看看Matchers 都有哪些API可供我们使用。

1
2
3
4
5
6
7
8
9
10
11
android.support.test.espresso.matcher
Classes
BoundedMatcher<T, S extends T> Some matcher sugar that lets you create a matcher for a given type but only process items of a specific subtype of that matcher.
CursorMatchers A collection of Hamcrest matchers that matches a data row in a Cursor.
CursorMatchers.CursorMatcher A Matcher that matches Cursors based on values in their columns.
LayoutMatchers A collection of hamcrest matches to detect typical layout issues.
PreferenceMatchers A collection of hamcrest matchers that match Preferences.
RootMatchers A collection of matchers for Root objects.
ViewMatchers A collection of hamcrest matchers that match Views.
Enums
ViewMatchers.Visibility Enumerates the possible list of values for View.getVisibility().

7个类,1个Eums。接下来我们一个个欣赏谷歌大神的杰作。

BoundedMatcher<T, S extends T>
一些匹配语法糖,允许您为给定类型创建匹配器,但只能处理该匹配器的特定子类型项。换句话说,就是能够自定义一些匹配器。
举个栗子,以下是一个自定义错误文本匹配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public final class ErrorTextMatchers {
/**
* Returns a matcher that matches {@link TextView}s based on text property value.
*
* @param stringMatcher {@link Matcher} of {@link String} with text to match
*/
@NonNull
public static Matcher<View> withErrorText(final Matcher<String> stringMatcher) {
return new BoundedMatcher<View, TextView>(TextView.class) {
@Override
public void describeTo(final Description description) {
description.appendText("with error text: ");
stringMatcher.describeTo(description);
}
@Override
public boolean matchesSafely(final TextView textView) {
return stringMatcher.matches(textView.getError().toString());
}
};
}
}

实现的主要细节如下。 我们通过从withErrorText()返回一个BoundedMatcher来确保匹配器只匹配TextView类的子类。 这使得很容易在BoundedMatcher.matchesSafely()中实现匹配逻辑本身:只需从TextView中获取getError()方法并将其送入下一个匹配器。 最后,我们有一个简单的describeTo()方法的实现,它只用于生成调试输出到控制台。
CursorMatchers
Hamcrest的集合匹配器,在Cursor匹配相应的数据行。
源码如下

1
2
3
4
5
6
7
8
9
10
/**
* Returns a matcher that matches a {@link String} value at a given column index
* in a {@link Cursor}s data row.
* <br>
* @param columnIndex int column index
* @param value a {@link String} value to match
*/
public static CursorMatcher withRowString(int columnIndex, String value) {
return withRowString(columnIndex, is(value));
}

大部分的场景,大多发生于表单或者滚动menu时。

1
2
3
4
onData(
is(instanceOf(Cursor.class)),
CursorMatchers.withRowString("job_title", is("Barista"))
);

LayoutMatchers hamcrest的集合匹配以检测典型的布局问题。
例如匹配具有椭圆形文本的TextView元素。 如果文本太长,无法适应TextView,
它可以是椭圆形(’Too long’显示为’Too l …’或’… long’)或切断(’Too
long“显示为”Too l“)。 虽然在某些情况下可以接受,但通常表示不好的用户体验。

PreferenceMatchers hamcrest匹配器来匹配一组偏好。
Preference组件其实就是Android常见UI组件与SharePreferences的组合封装实现。
onData(Matchers.<Object>allOf(PreferenceMatchers.withKey("setting-name"))).perform(click());
PreferenceMatchers还有以下方法可以应用到其他场景

1
2
3
4
5
6
7
withSummary(final int resourceId)
withSummaryText(String summary)
withSummaryText(final Matcher<String> summaryMatcher)
withTitle(final int resourceId)
withTitleText(String title)
withTitleText(final Matcher<String> titleMatcher)
isEnabled()

RootMatchers Root对象的匹配器集合。
匹配root装饰视图匹配给定的视图匹配器。

1
2
3
onView(withText("Text"))
.inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
.perform(click());

RootMatchers还有以下方法可以应用到其他场景

Public methods
static Matcher isDialog()Matches Roots that are dialogs (i.e.)
static Matcher isFocusable()Matches Roots that can take window focus.
static Matcher isPlatformPopup()Matches Roots that are popups - like autocomplete suggestions or the actionbar spinner.
static Matcher isTouchable()Matches Roots that can receive touch events.
static Matcher withDecorView(Matcher decorViewMatcher)Matches Roots with decor views that match the given view matcher.

ViewMatchers 最重要也是应用最广的匹配器,通过一个或者多个来定位层级里面的控件。

Public methods
static void assertThat(String message, T actual, Matcher matcher) A replacement for MatcherAssert.assertThat that renders View objects nicely.
static void assertThat(T actual, Matcher matcher) A replacement for MatcherAssert.assertThat that renders View objects nicely.
static Matcher hasContentDescription() Returns an Matcher that matches Views with any content description.
static Matcher hasDescendant(Matcher descendantMatcher) Returns a matcher that matches Views based on the presence of a descendant in its view hierarchy.
static Matcher hasErrorText(String expectedError) Returns a matcher that matches EditText based on edit text error string value.
static Matcher hasErrorText(Matcher stringMatcher) Returns a matcher that matches EditText based on edit text error string value.
static Matcher hasFocus() Returns a matcher that matches Views currently have focus.
static Matcher hasImeAction(int imeAction) Returns a matcher that matches views that support input methods (e.g.
static Matcher hasImeAction(Matcher imeActionMatcher) Returns a matcher that matches views that support input methods (e.g.
static Matcher hasLinks() Returns a matcher that matches TextViews that have links.
static Matcher hasSibling(Matcher siblingMatcher) Returns an Matcher that matches Views based on their siblings.
static Matcher isAssignableFrom(Class<? extends View> clazz) Returns a matcher that matches Views which are an instance of or subclass of the provided class.
static Matcher isChecked() Returns a matcher that accepts if and only if the view is a CompoundButton (or subtype of) and is in checked state.
static Matcher isClickable() Returns a matcher that matches Views that are clickable.
static Matcher isCompletelyDisplayed() Returns a matcher which only accepts a view whose height and width fit perfectly within the currently displayed region of this view.
static Matcher isDescendantOfA(Matcher ancestorMatcher) Returns a matcher that matches Views based on the given ancestor type.
static Matcher isDisplayed() Returns a matcher that matches Views that are currently displayed on the screen to the user.
static Matcher isDisplayingAtLeast(int areaPercentage) Returns a matcher which accepts a view so long as a given percentage of that view’s area is not obscured by any other view and is thus visible to the user.
static Matcher isEnabled() Returns a matcher that matches Views that are enabled.
static Matcher isFocusable() Returns a matcher that matches Views that are focusable.
static Matcher isJavascriptEnabled() Returns a matcher that matches WebView if they are evaluating Javascript.
static Matcher isNotChecked() Returns a matcher that accepts if and only if the view is a CompoundButton (or subtype of) and is not in checked state.
static Matcher isRoot() Returns a matcher that matches root View.
static Matcher isSelected() Returns a matcher that matches Views that are selected.
static Matcher supportsInputMethods() Returns a matcher that matches views that support input methods.
static Matcher withChild(Matcher childMatcher) A matcher that returns true if and only if the view’s child is accepted by the provided matcher.
static Matcher withClassName(Matcher classNameMatcher) Returns a matcher that matches Views with class name matching the given matcher.
static Matcher withContentDescription(int resourceId) Returns a Matcher that matches Views based on content description property value.
static Matcher withContentDescription(String text) Returns an Matcher that matches Views based on content description property value.
static Matcher withContentDescription(Matcher<? extends CharSequence> charSequenceMatcher) Returns an Matcher that matches Views based on content description property value.
static Matcher withEffectiveVisibility(ViewMatchers.Visibility visibility) Returns a matcher that matches Views that have “effective” visibility set to the given value.
static Matcher withHint(Matcher stringMatcher) Returns a matcher that matches TextViews based on hint property value.
static Matcher withHint(int resourceId) Returns a matcher that matches a descendant of TextView that is displaying the hint associated with the given resource id.
static Matcher withHint(String hintText) Returns a matcher that matches TextView based on it’s hint property value.
static Matcher withId(Matcher integerMatcher) Returns a matcher that matches Views based on resource ids.
static Matcher withId(int id) Same as withId(is(int)), but attempts to look up resource name of the given id and use an R.id.myView style description with describeTo.
static Matcher withInputType(int inputType) Returns a matcher that matches InputType.
static Matcher withParent(Matcher parentMatcher) A matcher that accepts a view if and only if the view’s parent is accepted by the provided matcher.
static Matcher withResourceName(String name) Returns a matcher that matches Views based on resource id names, (for instance, channel_avatar).
static Matcher withResourceName(Matcher stringMatcher) Returns a matcher that matches Views based on resource id names, (for instance, channel_avatar).
static Matcher withSpinnerText(int resourceId) Returns a matcher that matches a descendant of Spinner that is displaying the string of the selected item associated with the given resource id.
static Matcher withSpinnerText(String text) Returns a matcher that matches Spinner based on it’s selected item’s toString value.
static Matcher withSpinnerText(Matcher stringMatcher) Returns a matcher that matches Spinners based on toString value of the selected item.
static Matcher withTagKey(int key) Returns a matcher that matches View based on tag keys.
static Matcher withTagKey(int key, Matcher objectMatcher) Returns a matcher that matches Views based on tag keys.
static Matcher withTagValue(Matcher tagValueMatcher) Returns a matcher that matches Views based on tag property values.
static Matcher withText(Matcher stringMatcher) Returns a matcher that matches TextViews based on text property value.
static Matcher withText(String text) Returns a matcher that matches TextView based on its text property value.
static Matcher withText(int resourceId) Returns a matcher that matches a descendant of TextView that is displaying the string associated with the given resource id.

ok 这次主要介绍Matchers的API 更多的内容 大家还是要查看官方API去学习。
以下是android espresso matchers的地址
espresso matchers

QA请勿忘初心

发表于 2016-12-13

首先让我们回顾一下QA与QC的区别:

Quality Assurance :The planned and systematic activities implemented in a quality system so that quality requirements for a product or service will be fulfilled.
Quality Control :The observation techniques and activities used to fulfill requirements for quality.

QA的工作涉及软件研发流程的各个环节,且涉及到每一位参与研发的人员,但质量保证工作又不涉及具体的软件研发细节,侧重于整个流程。

QC则侧重于点,利用各种方法去检查某个功能是否满足业务需求。

thoughtworks 的QA则是这两者的混合体,你既要保证开发流程的质量,又要保证story的功能的是否正确。


来thoughtworks已经2年了,当过bqconf讲师与主持,参加过公司内各类测试相关活动,也阅读过g邮件中分享的关于test的话题,大部分人关注点都离不开自动化测试,面试的QA也说想到thoughtworks来学习高深的自动化测试,仿佛自动化测试代表了整个QA界,我反对盲目的自动化测试,确切的说反对盲目的UI自动化测试。很多QA在自动化测试海洋里迷失了自己。
我要强调自动化测试: 真的没有银弹。

QA的最终价值体现

Faster Delivery Of Quality Software From Idea To Consumer

确保项目的正确性

所以自动化测试只是其中的一小部分。
如上图顶部和底部的文字是对一个QA所能带给项目的总结:“我们在开发正确的产品吗?如果是,那么我们开发的产品正确吗?”所以QA首先需要在整个个项目过程中不断询问的所有成员上述问题,确保团队是在开发客户所需的产品,而不是自己YY出来的产品。

确保流程的正确性

Quality is not just in the software but also in the process

质量从来都不只是QA的职责,而是整个团队的职责。但QA如果自己都不注重,不督促组内成员改进质量,再将责任强加于整个团队,那么产品质量又何谈提升与保证。
中间的图片从一个QA的角度表明了一个用户故事的生命周期以及QA如何参与其中每个环节。


首先BA和客户将要开发的story列出之后,BA与QA可以一起pair编写具体story的内容,场景与验收条件,利用自己对业务以及系统的熟悉度,尽量的配合BA将story中坑尽量排除掉。


所有参与kick off 角色,都应该提前了解story内容。在kick off过程中,提出自己对story疑问。尽量将业务需求上问题在这个阶段解决。

在完成kick off后,QA可以和dev一起pair完成编写unit test 以及Automated Acceptance Tests,身为一个敏捷QA,我们起码要了解团队选用的单元测试工具,熟悉项目的技术架构,这样更好的便于我们对整个项目质量把控,在与dev pair的过程中,即帮助dev分析业务场景的分支,来确保单元测试覆盖的是正确的场景,而不是为了交代上级随便乱写的单元测试,也帮助QA熟悉代码,提高编码能力。

当DEV完成编码工作后,这时QA UX BA DEV一起检查story,是否按照story AC来检查是否完成对应的功能。UX也可发表对story UI以及交互一些看法,有任何问题及时讨论后,将问题尽早的反馈给客户。

当开发交付一部分功能之后,QA就可以做常规的用户故事测试,几个迭代之后,QA开始进行跨功能需求测试和探索性测试等。根据探索性测试的结果,QA可能会调整测试策略,调整测试优先级,完善测试用例等等。

上面这些QA实践貌似已经很完美,其实还差最重要的一环 quality analysis 。每次release后,我们总以为我们发布一个完美的产品,但却总能在新迭代开发过程中发现之前问题,历史总是惊人的相似,为什么,没有分析总结问题,以及相应的预防手段,那么同样的问题只会重现。
同时我们也要回顾下自己在工作中真的将这些敏捷实践都应用到工作中吗,我想或多或少的都有所欠缺。对于一个QA来说,不应循规蹈矩照搬敏捷实践。例如,在kickoff中,发现dev,UX对story涉及的场景以及内容了解不清楚,QA也可能漏掉一些测试场景,那么我们可以在kickoff之前,加入一个pre- kick off的实践,留出时间,让每个角色都能够完整了解story。在kick off之中,ux没有办法完整的确认页面的字体大小或者颜色等是否正确,那么在sign off之后,我们也加入一个UX-test实践,帮助UX能够更好解决这些问题。
所以每个项目也都有应适合自己项目的敏捷实践,发现项目存在的问题,持续改进才是最佳实践。

再来谈谈自动化测试吧。

pyramid
上面的测试金字塔对于大家来说再熟悉不过了,对于自动化测试来说最有价值的仍然是单元测试,但对于QA来说无疑最复杂的。
大部分QA或者tester,仍然以UI自动化为重心。之所以反对盲目的UI自动化测试,因为变化频繁的UI设计,极低投入产生比,都应该让我们重新思考下UI自动化的价值。

我们需要一个实施UI自动化正确的方式:

  • 能不用UI自动化测试就不用,梳理业务主线,只保留用户操作最频繁,交互最多的场景。
  • 根据面向对象设计的原则,构建适合项目的UI自动化框架,无论自己编写框架,还是采用开源框架。
  • 尽量采用独立测试数据,确保运行测试不受影响。例如采用mock数据库或者每次运行时还原测试数据库。

    回到正题,面对自动化测试的大潮,QA应该关注什么?

  • 编码规范,真实例子,dev对于类名命名没有用Camel-Case,造成在linux系统中部署不成功,python中乱使用缩进等。 其实都可以避免到,例如开发工具加入自动检查,或者在CI上加入校验编码规范的步骤,采用一些工具就可以达到目的,jshint,RuboCop等。
  • pair完成单元测试或API测试等,一方面可以提高QA的编码能力,另一面可以给出dev一些建议,将单元测试覆盖到更多的场景。
    例如,如果你们项目采用react作为前端框架,如果你不能理解react virtual dom 与jsx,当我们在写UI自动化脚本时,你会发现根本无法进行下去,日常中我们定位元素全是这种
    1
    2
    3
    4
    5
    6
    7
    <div class="styles__formField___1fyGy">
    <input type="text" placeholder="Email">
    <svg class="styles__formIcon___37VGd" viewBox="0 0 24 24" style="display: inline-block; fill: rgba(0, 0, 0, 0.870588); height: 24px; width: 24px; user-select: none; transition: all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms;">
    <path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z">
    </path>
    </svg>
    </div>

,所有的页面都是js渲染出来的,如果你懂jsx,就知道只需要在对于的Component render方法中更改加入id等元素就可以搞定

1
2
3
4
5
6
7
render() {
return (
<div>
<input type="text" placeholder="Email" id="Email">
</div>
)
}

  • 控制单元测试覆盖率,100%的单元测试覆盖率当然是最好的,但如果交付压力大,和客户商量后,我们可以尽量覆盖业务主线,而不是为了达到覆盖率延误了交付周期。

再来谈谈质量分析。

作为一个QA,我们不仅要检测项目中存在问题,也要改进团队的实践活动,更重要的是预防问题的发生。

  • 每次bugbash或相应迭代完成后,要分析统计,找出产生缺陷的环节,并采取措施防止问题再现。例如每次release或者bug bash之后,我可以按照功能模块与bug类型进行统计划分,分析统计bug的成因,例如某次迭代我们bug数量激增,经调查,发现我们对某些模块的前端代码进行了重构,但缺乏相应的单元测试与集成测试,造成了我们没有及时发现bug。之后我们就对应的采取措施防止问题再现。
  • 总结分析报告,及时反馈这些信息给团队。总结分析是一个长期的任务,每次bug数量的变动,都会直接体现整个团队上次迭代的开发质量,例如bug数量减少了,可以鼓励成员再接再厉。或者某几次迭代某些模块bug成上升趋势,那么就需要组织团队一起讨论问题根源,采取措施防止问题重现。
  • 利用代码质量分析工具帮助我们尽早预防问题的发生。例如sonar代码质量管理平台,可以帮助我们从代码复杂度,重复代码,单元测试覆盖率,编码规范等纬度来分析我们代码所存在的问题。当然也有其他的开源工具,像RubyCritic,/plato不同的语言都会有相应的工具。
  • 在线监控,利用像newrelic,airbnb等监控工具对部署在本地或在云中的web应用程序进行监控、故障修复、诊断、线程分析以及容量计划。这样就算们产品环境有任何问题,我们都会及时响应,尽早修复,减低损失。

最后让我们在看看QA应具有那些能力与技能.

qa_capablities
qa_skills

软技能方面包括风险控制,辅导他人,沟通能力,分析定位等。技能方面则包括缺陷管理,流程改进,测试分析,可用性测试,性能测试,安全测试等。

写在最后

回顾上面这些实践,其实我们可以做的更好,而不是把团队的质量全都交给自动化,回归QA的应有的初心,让我们从各个方面改进质量,带给团队更好的未来。

123…5
seven

seven

小qa 在thoughtworks苦苦挣扎中

42 日志
16 标签
RSS
© 2017 seven
由 Hexo 强力驱动
主题 - NexT.Pisces