试用PowerShell上的Pester 3

唔,说到行为驱动开发(BDD),这边倒完全没有接触过。之前得知PowerShell就有一个BDD框架,而且还是默认内置的,那我得试一下了。

概念

BDD(Behavior-driven developmentビヘイビア駆動開発 または 振舞駆動開発),这玩意儿我也不是很懂,总之感觉上是,在某种情况下,这功能应该怎样。具体的拜托查一下百科咯,或者点这里

环境

OS是Windows 10 20H2,$PSCulture$PSUICulture均为ja-JP,PowerShell版本为7.1.3,Pester版本为3.4.0(可以试着用Get-Module -Name Pester -ListAvailable看一下。之所以没用最新版,是因为3这个版本是Win 10内置的,即使是在不方便的现场也可以用。不过问题是和新版通常的写法有较大不同,但暂且不管)。

参考

项目

说明

哎,我有时候连自己的年龄都不知道,被别人问到的时候很尴尬。真的w

所以就想做一个能大致计算年龄的功能给自己用。

具体行为可以看之后写的测试。

结构

想弄得规整点,于是决定做一个模块(Module)出来,以后Import-Module再调用就是。

  • D:
    • prj
      • AgeCalc
        • AgeCalc.psm1
        • AgeCalc.Tests.ps1

以上文件,那个.psm1的就是要做的模块文件,而.Tests.ps1就是要写的测试。

其实应该先写出测试再把模块写出来,但这个版本Pester的用法还不够熟悉,所以实际上先把模块文件起了个头,保证能跑起来,再去写的测试。

搭建测试

先放代码吧。

AgeCalc.Tests.ps1


Describe 'Init' {
    BeforeAll {
        Get-Module -Name AgeCalc -All
            | Remove-Module -Force -ErrorAction Ignore
        Import-Module -Name .\AgeCalc.psm1 -Force -ErrorAction Stop
    }
    It 'Should have a function called "Get-Age"' {
        { Get-Command -Name 'Get-Age' -ErrorAction Stop } | Should Not Throw
    }
}

InModuleScope AgeCalc {

Describe 'OK' {
    BeforeAll {
        Mock Get-Date { return [datetime] '2021/03/14 17:15:00' }
    }
    Context 'It''s before or just on the birthday of this year' {
        It 'Should calculate' {
            Get-Age 2020-02-14 | Should Be 1
            Get-Age 20200313 | Should Be 1
            Get-Age 2020/03/14 | Should Be 1
        }
    }
    Context 'It''s after the birthday of this year' {
        It 'Should calculate' {
            Get-Age 2020-04-11 | Should Be 0
            Get-Age 2020/03/15 | Should Be 0
        }
    }
}

Describe 'NG' {
    Context 'Wrong format' {
        It 'Should mention the input is not proper' {
            { Get-Age PublicStaticVoidMain } | Should Not Throw
            { Get-Age 20210334 } | Should Not Throw
            { Get-Age 2021-0314 } | Should Not Throw
        }
    }
    Context 'Invalid input' {
        It 'Should mention the input should be earlier than now' {
            { Get-Age 2021-03-15 } | Should Not Throw
        }
    }
}

}

慢慢说。

我们要做的这个,计算年龄的话要根据当前的日期,以及用户输入的生年月日(せいねんがっぴ,如果喜欢这么读的话)。生年月日早于当前日期视为不合法(咱就不给整个负数了);今年没过生日不给长一岁,过了才长——如果是当前或之前年份的今天出生,那就当它过了生日。

也就是说,如果今天是2021-03-14,那些今年已经过了生日或者就今天生日的,直接年份相减也可以,比如测试中的2020-02-14202003132020/03/14,都应该为1岁;那些今年没过生日的,自然不能单纯年份相减,还得扣一年才行,比如测试中的2020-04-112020/03/15,都应该为0岁才对。

不过这测试写得有点少,或许还得加一些,毕竟测试结果除了0就是1呢w

搭建模块

虽说要写模块,不过模块本身写得并不规范,只是规范化了一下函数的写法而已。

AgeCalc.psm1

Set-StrictMode -Version Latest

<#
.Synopsis
Calculates your age roughly.
.Description
Calculates your age roughly.
Based on the date of now.
Dates of birth after now are not supported.
.Parameter DateOfBirth
Specifies the date of birth.
Should be a string.
Only formats "yyyyMMdd", "yyyy-MM-dd", "yyyy/MM/dd" are supported.
.Inputs
None. Cannot pipe objects in it.
.Outputs
System.Int32. Returns the number of age.
.Example
PS> '{0:yyyy-MM-dd}' -f (Get-Date)
2021-03-14
PS> Get-Age -DateOfBirth 2020-02-15
1
.Example
PS> '{0:yyyy-MM-dd}' -f (Get-Date)
2021-03-14
PS> Get-Age 2011/03/18
10
#>
function Get-Age {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]
        $DateOfBirth
    )
    begin {
        [datetime] $dtDateOfBirth = [datetime]::new($null)
        [string[]] $formats = 'yyyyMMdd', 'yyyy-MM-dd', 'yyyy/MM/dd'
        [string] $errFormat = @"
The input format should be "$($formats[0])", "$($formats[1])" or "$($formats[2])".
"@
        [string] $errInvalid = @"
The input should be earlier than now.
"@
        if (![datetime]::TryParseExact(
                $DateOfBirth,
                $formats,
                (Get-Culture),
                [System.Globalization.DateTimeStyles]::None,
                [ref] $dtDateOfBirth)) {
            Write-Error `
                -Message $errFormat `
                -ErrorAction Stop
        }
    }
    end {
        [datetime] $dtDateOfNow = (Get-Date).Date

        if ($dtDateOfBirth -gt $dtDateOfNow) {
            Write-Error `
                -Message $errInvalid `
                -ErrorAction Stop
        }

        [int] $age = $dtDateOfNow.Year - $dtDateOfBirth.Year
        if ($dtDateOfBirth.Month -gt $dtDateOfNow.Month) {
            $age--
        } elseif ($dtDateOfBirth.Month -eq $dtDateOfNow.Month) {
            if ($dtDateOfBirth.Day -gt $dtDateOfNow.Day) {
                $age--
            }
        }
        return $age
    }
}

Export-ModuleMember -Function Get-Age

接着说。

方便起见,唯一要传的参数就只有DateOfBirth——生年月日,它的格式如上所述,yyyyMMdd的不分隔、横杠(hyphen)分隔、斜杠(slash)分隔这三种形式而已,别的一概不收。

实现本身倒是没什么可说,注意别弄反了就是。

测试兼代码覆盖率测试

呃,“代码覆盖率(Code Coverage)”……行吧爱谁谁,先跑。

D:
Set-Location .\prj\AgeCalc
Invoke-Pester .\AgeCalc.Tests.ps1 -CodeCoverage .\AgeCalc.psm1

结果如下:

Describing Init
 [+] Should have a function called "Get-Age" 54ms
Describing OK
   Context It's before or just on the birthday of this year
    [+] Should calculate 72ms
   Context It's after the birthday of this year
    [+] Should calculate 47ms
Describing NG
   Context Wrong format
    [+] Should mention the input is not proper 75ms
   Context Invalid input
    [+] Should mention the input should be earlier than now 44ms
Tests completed in 294ms
Passed: 5 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0

Code coverage report:
Covered 100.00% of 23 analyzed commands in 1 file.

看样子是成了,覆盖率也是百分百。

后记

话说回来,只是单纯想试试。

干活累了经常会尝试一些感觉新鲜的东西,经常有启发,兴许对之后的工作也有助益。

InSb

InSb

只是跟工作和生活相关的记录