2016年11月8日 星期二

specflow 如何自動產生文件規格

剛進公司就有91的訓練課程,剛好有機會接觸BDD的全貌流程,從完全不理解為何步驟如此頻繁透過工具簡化許多手動的操作的動作,進而提昇化工作效率的過程,以免日後忘了這個好用的東西,簡單作個分享囉~

一、外掛SpecFlow:
這是cucumber支援.net的擴充套件,真是佛心來的,可以從使用者的情境角度,直接相對應的產生測試案例,這比之前微軟推出的單元測試更直覺,還能重複使用情境案例


























安裝Nuget 套件
  1. NUnit
  2. SpecFlow
二、建立需求檔



新增Feature 的檔案後,要修改app.config 設定檔 --> 找到specFlow 的xml,修改如下所示
<specFlow>
     <unitTestProvider name="MsTest"></unitTestProvider>
</specFlow>



Feature.cs 預設以下的內文
Feature: example
In order to avoid silly mistakes (說明想做什麼)
As a math idiot (角色)
I want to be told the sum of two numbers (期望要驗證的事)

@mytag (允許將情境做分類)
Scenario: Add two numbers (情境說明要做什麼事)
Given I have entered 50 into the calculator (要加入的參教為 50 )
And I have entered 70 into the calculator (另外一個參教為 70 )
When I press add (執行add事件)
Then the result should be 120 on the screen (期望結果傳回120)

根據說明,修改我們的Feature 檔如下 :
舉例: 如果客戶的資料檔中如果共計有五筆資料,輸入查詢區間為2014-07-03~ 2014-07-07查詢結果為只有只有符合該區間的筆數共3筆,驗證其結果是否符合預期。


Scenario: 依據姓名、訂單日期起、訂單日期迄條件,查出顧客的訂單 (情境說明要做什麼事)
Given 查詢條件為 (要加入的參教為 資料表(CustomerId | OrderDateStart | OrderDateEnd)
| CustomerId | OrderDateStart | OrderDateEnd |
| Joey | 2014-07-03 | 2014-07-07 |
And 預計Customers資料應有 (要加入的參教為 資料表(CustomerID | CompanyName )
| CustomerID | CompanyName |
| Joey | SkillTree |
And 預計Orders資料應有 (要加入的參教為 資料表(CustomerID | OrderDate | ShipCity )
| CustomerID | OrderDate | ShipCity |
| Joey | 2014-07-02 | Taipei |
| Joey | 2014-07-03 | Taipei |
| Joey | 2014-07-04 | Changhua |
| Joey | 2014-07-05 | Changhua |
| Joey | 2014-07-08 | Changhua |
When 呼叫Query方法 (要執行 Query 的方法 )
Then 查詢結果應為 (預期傳回的結果 )
| CustomerID | OrderDate | ShipCity |
| Joey | 2014-07-03 | Taipei |
| Joey | 2014-07-04 | Changhua |
| Joey | 2014-07-05 | Changhua |

當我們的條件模擬狀況寫完了之後… 然到還要回到測試案例寫測試程式嗎當然不用啦Featur 的內文,按下【右鍵】產生Generate Stdep Definition 就產生對應的測試案例囉先給個讚吧!


按下右鍵 go to stdep Definition (到測試程式的函式名稱)
PS:測試內容當然要自已寫囉~~ 所以以下示範如何寫測試程式
[Binding]
[Scope(Feature = "OrderQuery")]
public class OrderQuerySteps
{
private OrderService target;
private NorthwindEntitiesInTest dbContext;
//在所有測試案例起動之前,執行此區塊,目的用來先清掉之前的遺留假資料
[BeforeScenario]
public void BeforeScenario()
{
this.target = new OrderService();
//為了直接測試資料庫的真實行為 呼叫entity 實體資料庫
using (dbContext = new NorthwindEntitiesInTest())
{
dbContext.Database.ExecuteSqlCommand("Delete [Orders] Where CustomerID IN ('Joey','JoeyTest')");
dbContext.Database.ExecuteSqlCommand("Delete [Customers] Where CustomerID IN('Joey','JoeyTest')");
}
}

//在所有測試案例起動之後,執行此區塊,目的用來先清掉之前的遺留假資料
[AfterScenario]
public void AfterScenario()
{
using (dbContext = new NorthwindEntitiesInTest())
{
dbContext.Database.ExecuteSqlCommand("Delete [Orders] Where CustomerID IN ('Joey','JoeyTest')");
dbContext.Database.ExecuteSqlCommand("Delete [Customers] Where CustomerID IN('Joey','JoeyTest')");
}
}

[Given(@"查詢條件為")]
public void Given查詢條件為(Table table)
{
//傳入的table 型態,直接轉換成物件
var condition = table.CreateInstance<OrderQueryCondition>();
//要將查詢條件暫存記憶體
ScenarioContext.Current.Set<OrderQueryCondition>(condition);

}

[Given(@"預計Customers資料應有")]
public void Given預計Customers資料應有(Table table)
{
var customers = table.CreateSet<SpecFlowWithEf.Tests.ModelInTest.Customers>();

using (dbContext = new NorthwindEntitiesInTest())
{
foreach (var customer in customers)
{
dbContext.Customers.Add(customer);
}
dbContext.SaveChanges(); //要將客戶的假資料存到實體資料庫中
}
}


[Given(@"預計Orders資料應有")]
public void Given預計Orders資料應有(Table table)
{
var orders = table.CreateSet<SpecFlowWithEf.Tests.ModelInTest.Orders>();

using (dbContext = new NorthwindEntitiesInTest())
{
foreach (var order in orders)
{
dbContext.Orders.Add(order);
}
dbContext.SaveChanges(); //要將訂單的假資料存到實體資料庫中
}
}

[When(@"呼叫Query方法")]
public void When呼叫Query方法()
{
//取得查詢條件的資料
var condition = ScenarioContext.Current.Get<OrderQueryCondition>();
//service回傳的,不一定需要是 dbContext EF 產生的 model 型別
IEnumerable<MyOrder> actual = this.target.Query(condition);
//將回傳的結果儲存
ScenarioContext.Current.Set<IEnumerable<MyOrder>>(actual);
}

[Then(@"查詢結果應為")]
public void Then查詢結果應為(Table table)
{
//將預期的結果儲存
var actual = ScenarioContext.Current.Get<IEnumerable<MyOrder>>();
//比較結果
table.CompareToSet<MyOrder>(actual);
}
}
PS:[BeforeScenario] [AfterScenario] 是自已加上的區塊,跑所以測試案例之前 或 之後都執行的區塊,如果不需要用到,也就不用特別加了。 以上截取91的上課程式範例

四、用工具產生文件
如果寫完所有的測試案例之後,主管要求寫文件,這也令工程師頭痛的吧~ 所以也有人佛心的寫的套件,自動產生線上文件喔,前提是要有Feature檔和自動產生的測試程式,才能自動產生文件喔… 讓我們再來看看神奇的魔術吧~

下戴工具包

或者 nuget pickles

nuget picklesui 圖形化的線上文件產生器


feature Directory : 選擇Feature 檔的資料夾
output Directory : 選擇 匯出文件的目錄
project name : 專案名稱
project version : 版次

產生的OrderQuery的需求案例


結論: 各位看倌~ 你不用寫任何一行文件需求,自動生成的文件,隨著案例不同,文件也隨之自動產生,不需要SA 或 工程師辛苦的修改文件了… 多麼神奇啊,每一次的修改程式都有不如預期的地方,也許是邏輯不夠完善,透過每次的演化BDD的情境模擬,來讓程式更加完善,需求異動頻繁更需要測試,透過VS擴充工具讓原本無法預期的程式變得更可預測,縮短BUG的週期… 如果有不完善的地方,還期望各位多多的補充喔…


2016年10月16日 星期日

Reactive Native 初體驗

話說IONIC2 Angular2 的組合仍然是當前火紅的HyBrid APP, 但開發行動裝置的語言工具又多了一款React Native 可直接呼叫原生的API,而且語法近似乎HTML5 CSS3,所以讓我嘗鮮看看,找到一款完美的跨平台語法是每個人的夢想。

一、建立react native 的專案


react-native init AwesomeProject
cd AwesomeProject
react-native run-android

二、TabNavigator 範例:

適用於IOSAndroid Navigator Tab (下方的工具列表上,可切換的頁籤),因為是第三方元件提供的,所以要先安裝

npm install react-native-tab-navigator --save

範例: index.android.js 建立有兩個頁籤的TAB,分別是頁面Featured Search

首先開始使用react 要先import 基本元件:

import React, { Component,PropTypes } from 'react';
import {
AppRegistry, //用來預設畫面
StyleSheet, //如同css一樣定義文字大小、排版
Text, //顯示文字
View //每個畫面==>screen
} from 'react-native';
import TabNavigator from 'react-native-tab-navigator'; //引用外部的元件,所以要import

TabNavigator 的第三方元件可以自訂新增tab 頁籤,每頁籤可引用自訂react component 控件,例如以下引用兩個【Featured】和【Search】在不同component控件

import React, { Component,PropTypes } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
import TabNavigator from 'react-native-tab-navigator';

var Featured = require('./Featured');
var ListViewBasics = require('./ListViewDemo');

class AwesomeProject extends Component {
constructor(props) {
super(props);
this.state = {
selectedTab: 'Featured' //預設為Featured
};
}

render() {
return (
<TabNavigator>
    <TabNavigator.Item selected={this.state.selectedTab === 'Featured'}
      title="Featured" onPress={() => this.setState({ selectedTab: 'Featured' })}>
         <Featured />//零組件
   </TabNavigator.Item>
   <TabNavigator.Item selected={this.state.selectedTab === 'ListViewBasics'}
        title="ListViewBasics" onPress={() => this.setState({ selectedTab: 'ListViewBasics' })}>
        <ListViewBasics />   //零組件
   </TabNavigator.Item>
</TabNavigator>
);
}
}

AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); //用來當作預設頁的元件

設定 StyleSheet
const styles = StyleSheet.create({
container: {
    flex: 1, //用來在各種不同螢幕大小,元件layout與邊框距離
    justifyContent: 'center', //置中處理
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
},
welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
},
instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
},
});

三、Navigator (頁面的轉換路徑)
Navigator 元件允許頁面轉向到另一個新的頁面,接續上面的案例中,Featured是獨立的控件,要能導覽至下一頁MyScene,按下back就回上一頁,所以要先import Navigator元件和MyScene 控件,才能將畫面導覽至MyScene

Featured.js
'use strict';
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Navigator
} from 'react-native';
import MyScene from './MyScene'; //元件,要自已實作
var styles = StyleSheet.create({
    description: {
    fontSize: 20,
    backgroundColor: 'white'
},
container: {
   flex: 1,
   justifyContent: 'center',
   alignItems: 'center'
}
});
class Feature extends Component {
render() {
return (
<Navigator
initialRoute={{title:"my Initial Scene", index:0}} //傳給下一頁的參數值(Title,index)
renderScene={(route,navigator)=>{
return <MyScene title={route.title}
onForward={()=>{
const nextIndex=route.index+1;
    navigator.push({
    title:"Scene_"+nextIndex,
    index:nextIndex,
});
}}
onBack={() => {
if (route.index > 0) {
     navigator.pop();
}
}}
/>
}}
/>
);
}
}
module.exports = Feature;

名稱 MyScene的自訂屬性 onForward onBack,傳入函式用來控制導覽下一頁 navigator.push 的動作和上一頁 navigator.pop()的動作。

MyScene.js
import React, { Component,PropTypes } from 'react';
import { View, Text, Navigator,StyleSheet,TouchableHighlight } from 'react-native';

var styles = StyleSheet.create({
toolbar:{
backgroundColor:'#81c04d',
paddingTop:30,
paddingBottom:10,
flexDirection:'row' //用來控制以列(由左->)的排列
},
toolbarButton:{
width: 50,
color:'#fff',
textAlign:'center'
},
toolbarTitle:{
color:'#fff',
textAlign:'center',
fontWeight:'bold',
flex:1
}
});

class MyScene extends Component {

static PropTypes={
title: PropTypes.string.isRequired,
onForward:PropTypes.func.isRequired,
onBack:PropTypes.func.isRequired
}
render() {
return (
<View style={styles.toolbar}>
<TouchableHighlight onPress={this.props.onBack}>
<Text style={styles.toolbarButton}>back</Text>
</TouchableHighlight>
<Text style={styles.toolbarTitle}> {this.props.title}</Text>
<TouchableHighlight onPress={this.props.onForward}>
<Text style={styles.toolbarButton}>next</Text>
</TouchableHighlight>
</View>
)
}
}
module.exports=MyScene;

<TouchableHighlight> 控制允許點選按鈕範圍,所以如上所示 back next是按鈕,各分別按下後呼叫不同的事件產生, {this.props.onBack}{this.props.OnFoward} 調用MyScene傳入參數的OnFoward OnBack 的方法。



ListViewDemo.js

import React, { Component } from 'react';
import { AppRegistry, ListView,ScrollView,Image,TouchableOpacity,Text, View, StyleSheet } from 'react-native';

//自訂圖片的檔案路徑
var THUMB_URLS = [
require('./imgs/like.png'),
require('./imgs/dislike.png'),
require('./imgs/call.png'),
require('./imgs/fist.png'),
require('./imgs/bandaged.png'),
require('./imgs/flowers.png'),
require('./imgs/heart.png'),
require('./imgs/liking.png'),
require('./imgs/party.png'),
require('./imgs/poke.png'),
require('./imgs/superlike.png'),
require('./imgs/victory.png'),
];

class ListViewDemo extends Component {

constructor(props) {
super(props);
const ds = new ListView.DataSource({
    rowHasChanged: (r1, r2) => r1 !== r2,
    sectionHeaderHasChanged: (s1, s2) => s1 !== s2
});
this.state = {
dataSource: ds.cloneWithRowsAndSections(this.getRows()), //取得getRows()繫結資料
};
}
getRows(){
  let dataObj = {}
  let section = '测試1'
  dataObj[section] = []
  for (let i=0;i<10;i++){
     let data = {
     name:''+i+'',
     num:i
  }
   dataObj[section][i] = data
  }
  section = '测試2'
  dataObj[section] = []
  for (let i=0;i<10;i++){
     let data = {
     name:''+i+'',
     num:i
   }
  dataObj[section][i] = data
 }
 return dataObj
}
//rowData傳入的資料列(每筆資料物件,分組ID, 資列列索引, 是否選中的狀態)
renderRow(rowData,sectionID,rowID,highlightRow){
var imgSource = THUMB_URLS[rowID];
return (
  <View style={styles.rowItem}>
    <View style={styles.rowItemLeft}>
        <Image style={styles.thumb} source={imgSource} resizeMode={"cover"} />
    </View>
    <View style={styles.rowItemRight}>
        <Text style={styles.rowItemText}>{rowData.num}</Text>  
    </View>
  </View>
  )
}
onEndReached(e){
//當資料列執行最後一列的時候,執行此區塊
}
renderSectionHeader(sectionData, sectionID){ //用來顯示資料列的分組的標題(一維陣列的值)
return(
<View style={styles.rowTite}>
    <Text>{sectionID}</Text>
</View>
)
}
onChangeVisibleRows(visibleRows, changedRows){
//當資料列有變化時,呼叫此區塊
}
render() {
return (
<ListView
   style={styles.body}
   onEndReached = {this.onEndReached}
   onEndReachedThreshold = {20}
   renderSectionHeader = {this.renderSectionHeader}
   onChangeVisibleRows = {this.onChangeVisibleRows}
   dataSource={this.state.dataSource}
   renderRow={this.renderRow} />
   );
  }
}

var styles = StyleSheet.create({
body:{
flex:1,
},
rowItem:{
    flex:1,
    height:50,
    flexDirection:'row',
    justifyContent:'center',
    alignItems:'center',
    borderBottomWidth:1,
    borderBottomColor:'#ddd',
},
rowTite:{
    height:30,
    alignItems:'center',
    justifyContent:'center',
    backgroundColor:'#ccc',
},
rowItemLeft:{
   flex:1,
   borderRightColor:'#ccc',
},
rowItemRight:{
  flex:3,
},
rowItemText:{
  textAlign:'center'
},
thumb: {
  width: 30,
  height:30
},
});
module.exports = ListViewDemo;

RenderRow(rowData,sectionID,rowID,highlightRow)
rowData : 每筆資料列視為物件,可以呼叫出每個物件的屬性 ex: rowData.name
sectionID : 分組的ID
rowID : 顯示資料列的索引值,從0開始
highlightRow : 是否該資料列被選中



reactNative 的相關指令:

執行Android 的compile : react-native run-android
建議安裝Android 的模擬器,因為作者無法實機連線到react native ,搞了很久…


後來只好將 apk 安裝在實機上面測試  …  搜尋 \專案路徑 \android\app\build\outputs\apk\ app-debug.apk 透過網路 或USB直接安裝在手機上

最後文件中找到這樣一篇文章

A common issue is that the packager is not started automatically when you run react-native run-android. You can start it manually using react-native start.

下指令 : react-native start   (開始預設PORT 8081)



然後啟動APP就成功囉~謝謝各位


三、參考文獻