数学
Maths
現代のコンピューターのすべての能力が驚異的な速さで膨大な合計を実行するために、普通の開発者は自分の仕事を行うために数学を使用することはほとんどありません。
でも今日はだめ!
今日は、数学を使用して実際の問題を解決します。退屈な数学ではありません。 私たちは三角法やベクトルなど、高校の後で使う必要はないと言っていたあらゆる種類のものを使用します。
時計のSVGを作成したい。
デジタル時計ではありません。きっと、それは簡単でしょう。アナログ時計です。 あなたが求めているのは、
time
パッケージからTime
を受け取り、時、分、秒のすべての針が正しい方向を向いている時計の SVG を出力するだけの素敵な関数です。これがどれほど難しいことなのでしょうか?
最初に、遊ぶために時計の SVG が必要になります。 SVGは、XMLで記述された一連の形状として記述されているため、プログラムで操作する ための素晴らしい画像フォーマットです。
時計のSVG
このように記述されています。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 300 300"
version="2.0">
<!-- bezel -->
<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>
<!-- hour hand -->
<line x1="150" y1="150" x2="114.150000" y2="132.260000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
<!-- minute hand -->
<line x1="150" y1="150" x2="101.290000" y2="99.730000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
<!-- second hand -->
<line x1="150" y1="150" x2="77.190000" y2="202.900000"
style="fill:none;stroke:#f00;stroke-width:3px;"/>
</svg>
これは、3 本の線が描かれた円で、各線は円の真ん中にあり、(x=150、y=150)あり、少し離れています。
だから私たちがやろうとしていることは、上記をどうにかして再構築することですが、線を変更して、それらが所定の時間に適切な方向を指すようにします。
行き詰まる前に、受け入れテストについて考えてみましょう。時計の例があるので、重要なパラメータがどうなるかを考えてみましょう。
<line x1="150" y1="150" x2="114.150000" y2="132.260000"
style="fill:none;stroke:#000;stroke-width:7px;"/>
時計の中心(この線の属性
x1
およびy1
)は、時計の各針で同じです。 時計の針ごとに変更する必要がある数値(SVGを構築するためのパラメーター)は、x2
およびy2
属性です。時計の針ごとにX
とY
が必要です。私はより多くのパラメーターについて考えることができました。 文字盤の円の半径、SVGのサイズ、手の色、その形状など...しかし、シンプルで具体的な問題を解決することから始めるのが良いです。具体的な解決策、そしてそれを一般化するためにパラメーターを追加し始めます。
ので、箇条書きにします。
- すべての時計の中心は(150、150)です
- 時針は50です。
- 分針は80です
- 秒針は90秒です。
SVGに関する注意点:原点(0, 0)-は、予想される 左下 ではなく、 左上 です。どの番号をラインに接続するかを検討しているときは、これを覚えておくことが重要です。
最後に、SVGを構築する how は決めていません。
text/template
パッケージのテンプレートを使用するか、単にバイトをbytes.Buffer
またはライターに入れます。しかし、これらの数値が必要になることはわかっているので、それらを作成するもののテストに焦点を合わせましょう。だから私の最初のテストは次のようになります。
package clockface_test
import (
"testing"
"time"
"github.com/gypsydave5/learn-go-with-tests/math/v1/clockface"
)
func TestSecondHandAtMidnight(t *testing.T) {
tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)
want := clockface.Point{X: 150, Y: 150 - 90}
got := clockface.SecondHand(tm)
if got != want {
t.Errorf("Got %v, wanted %v", got, want)
}
}
SVGが左上から座標をプロットする方法を覚えていますか? 秒針を真夜中に配置するには、X軸(まだ150)の文字盤の中心から移動しておらず、Y軸は中心からの「上」方向の長さです。 150 マイナス 90。
これにより、不足している関数と型の周囲で予想される失敗が排除されます。
--- FAIL: TestSecondHandAtMidnight (0.00s)
# github.com/gypsydave5/learn-go-with-tests/math/v1/clockface_test [github.com/gypsydave5/learn-go-with-tests/math/v1/clockface.test]
./clockface_test.go:13:10: undefined: clockface.Point
./clockface_test.go:14:9: undefined: clockface.SecondHand
FAIL github.com/gypsydave5/learn-go-with-tests/math/v1/clockface [build failed]
だから秒針の先が行くはずの
Point
とそれを取得する関数。これらの型を実装して、コードをコンパイルしてみましょう
package clockface
import "time"
// A Point represents a two dimensional Cartesian coordinate
type Point struct {
X float64
Y float64
}
// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand(t time.Time) Point {
return Point{}
}
そして今、私たちは
--- FAIL: TestSecondHandAtMidnight (0.00s)
clockface_test.go:17: Got {0 0}, wanted {150 60}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v1/clockface 0.006s
予想される失敗が発生したら、
HandsAt
の戻り値を入力できます。// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand(t time.Time) Point {
return Point{150, 60}
}
見てください。テストが成功しました。
Behold, a passing test.
PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v1/clockface 0.006s
まだリファクタリングする必要はありません。コードはほとんどありません!
毎回真夜中を示す時計を返すだけではなく、おそらくここでいくつかの作業を行う必要があります...
func TestSecondHandAt30Seconds(t *testing.T) {
tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC)
want := clockface.Point{X: 150, Y: 150 + 90}
got := clockface.SecondHand(tm)
if got != want {
t.Errorf("Got %v, wanted %v", got, want)
}
}
同じアイデアですが、今度は秒針が downwards を指しているため、Y軸の長さを add します。
これはコンパイルされます...しかし、どのように成功したのでしょうか?
この問題をどのように解決しますか?
秒針は毎分同じ60の状態を通過し、60の異なる方向を指しています。 0秒の場合は文字盤の上部を指し、30秒の場合は文字盤の下部を指します。簡単です。
つまり、秒針がどの方向を向いているか、たとえば37秒を考えたい場合は、円の周りの12時から37/60度の間の角度が必要です。 度数では、これは
(360 / 60 ) * 37 = 222
ですが、完全なローテーションの37/60
であることを覚えているだけの方が簡単です。しかし、角度はストーリーの半分にすぎません。秒針の先端が指しているX座標とY座標を知る必要があります。どうすればそれを解決できますか?
原点の周りに描かれた半径1の円を想像してください。座標
0, 0
。
単位円の画像
これは「単位円(
unit circle
)」と呼ばれます。半径が1単位だからです。円の円周は、グリッド上のポイントから作られます。 より多くの座標。これらの各座標のxおよびyコンポーネントは三角形を形成し、その斜辺は常に1。 円の半径です

円周上にポイントが定義されている単位円の画像
三角測量では、原点との角度がわかっている場合、各三角形のXとYの長さを計算できます。 X座標はcos(a)になり、Y座標はsin(a)になります。ここで、aは線と(positive)x軸とのなす角度です。

それぞれcos(a)とsin(a)として定義された光線のx要素とy要素を含む単位円の画像。ここで、aはx軸と光線がなす角度
最後のひねりX軸からではなく12時からの角度を測定したいので、(3時)、軸を交換する必要があります。ここで、x = sin(a)およびy = cos(a)です。

y軸からの角度で定義された単位円光線
これで、秒針の角度(1秒ごとに円の1/60)とX座標とY座標を取得する方法がわかりました。
sin
と cos
の両方に関数が必要です。Cosは、ラジアン引数xの余弦を返します。
角度をラジアンにする必要があります。では、ラジアンとは何ですか?円の完全な回転を360度で構成するのではなく、完全な回転を2πラジアンとして定義します。これを実行する理由はありません。
読んだり、学習したり、考えたりしたので、次のテストを書くことができます。
このすべての数学は困難で混乱を招きます。私は何が起こっているのか理解していると確信していません。 それではテストを書きましょう!問題全体を一度に解決する必要はありません。特定の時間の秒針について、ラジアン単位で正しい角度を計算することから始めましょう。
私はこれらのテストを
clockface
パッケージ内で記述します。それらがエクスポートされることはなく、何が起こっているのかをしっかり把握すると、または移動されて削除される可能性があります。また、これらのテストに取り組んでいる間に取り組んでいた受け入れテストをコメントアウトします 。これに合格している間、そのテストに気を取られたくありません。
package clockface
import (
"math"
"testing"
"time"
)
func TestSecondsInRadians(t *testing.T) {
thirtySeconds := time.Date(312, time.October, 28, 0, 0, 30, 0, time.UTC)
want := math.Pi
got := secondsInRadians(thirtySeconds)
if want != got {
t.Fatalf("Wanted %v radians, but got %v", want, got)
}
}
ここでは、1分の30秒後に秒針が24時間半になることをテストしています。そして、それは
math
パッケージの最初の使用です!円の1回転が2πラジアンである場合、途中のラウンドはちょうどπラジアンになるはずです。math.Pi
はπの値を提供します。# github.com/gypsydave5/learn-go-with-tests/math/v2/clockface [github.com/gypsydave5/learn-go-with-tests/math/v2/clockface.test]
./clockface_test.go:12:9: undefined: secondsInRadians
FAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface [build failed]
func secondsInRadians(t time.Time) float64 {
return 0
}
--- FAIL: TestSecondsInRadians (0.00s)
clockface_test.go:15: Wanted 3.141592653589793 radians, but got 0
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.007s
func secondsInRadians(t time.Time) float64 {
return math.Pi
}
PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.011s
まだリファクタリングは必要ありません。
テストを拡張して、さらにいくつかのシナリオをカバーできます。 少し先にスキップして、すでにリファクタリングされたテストコードをいくつか示します。目的の場所に到達する方法は十分に明確になっているはずです。
func TestSecondsInRadians(t *testing.T) {
cases := []struct {
time time.Time
angle float64
}{
{simpleTime(0, 0, 30), math.Pi},
{simpleTime(0, 0, 0), 0},
{simpleTime(0, 0, 45), (math.Pi / 2) * 3},
{simpleTime(0, 0, 7), (math.Pi / 30) * 7},
}
for _, c := range cases {
t.Run(testName(c.time), func(t *testing.T) {
got := secondsInRadians(c.time)
if got != c.angle {
t.Fatalf("Wanted %v radians, but got %v", c.angle, got)
}
})
}
}
いくつかのヘルパー関数を追加して、このテーブルベースのテストの作成を少し面倒にしました。
testName
は時刻をデジタル時計形式(HH:MM:SS)に変換し、simpleTime
は実際に気にする部分のみを使用してtime.Time
を構築します(再び、時間、分、秒)。func simpleTime(hours, minutes, seconds int) time.Time {
return time.Date(312, time.October, 28, hours, minutes, seconds, 0, time.UTC)
}
func testName(t time.Time) string {
return t.Format("15:04:05")
}
これらの2つの関数は、これらのテスト(および将来のテスト)の記述と保守を少し簡単にするのに役立ちます。
これにより、優れたテスト出力が得られます。
--- FAIL: TestSecondsInRadians (0.00s)
--- FAIL: TestSecondsInRadians/00:00:00 (0.00s)
clockface_test.go:24: Wanted 0 radians, but got 3.141592653589793
--- FAIL: TestSecondsInRadians/00:00:45 (0.00s)
clockface_test.go:24: Wanted 4.71238898038469 radians, but got 3.141592653589793
--- FAIL: TestSecondsInRadians/00:00:07 (0.00s)
clockface_test.go:24: Wanted 0.7330382858376184 radians, but got 3.141592653589793
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v3/clockface 0.007s
上で話していた数学のすべてを実装するお時間です。
func secondsInRadians(t time.Time) float64 {
return float64(t.Second()) * (math.Pi / 30)
}
1秒は(2π/60)ラジアンです... 2をキャンセルすると、π/30ラジアンになります。これに秒数(
float64
として)を掛けると、すべてのテストに合格するはずです...--- FAIL: TestSecondsInRadians (0.00s)
--- FAIL: TestSecondsInRadians/00:00:30 (0.00s)
clockface_test.go:24: Wanted 3.141592653589793 radians, but got 3.1415926535897936
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v3/clockface 0.006s
待って、なになに?
浮動小数点演算は悪名高いほど不正確です。 コンピュータは本当に整数とある程度の有理数しか扱えません。特に、
secondsInRadians
関数のように10進数を上下に因数分解すると、10進数は不正確になり始めます。 math.Pi
を30で除算してから30を掛けることで、math.Pi
と同じではない数値になりました。これを回避するには2つの方法があります。
- 1.それに活かす
- 2.方程式をリファクタリングして関数をリファクタリングする
(1)はそれほど魅力的ではないように思われるかもしれませんが、浮動小数点の等価性を機能させる唯一の方法であることがよくあります。時計の文字盤を描画するために、ごくわずかな分数で不正確であることは率直に言って問題にならないので、角度に対して「十分に近い」等式を定義する関数を書くことができます。
しかし、精度を取り戻すには簡単な方法があります。方程式を並べ替えて、分割して乗算しないようにします。分割するだけですべてが可能です。
だから代わりに
numberOfSeconds * π / 30
私たちは書けます。
π / (30 / numberOfSeconds)
これは同等です。
In Go:
func secondsInRadians(t time.Time) float64 {
return (math.Pi / (30 / (float64(t.Second()))))
}
そして、パスが成功します。
PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v2/clockface 0.005s
したがって、ここで最初の部分をカバーしました。 秒針がラジアンで指し示す角度はわかっています。次に、座標を計算する必要があります。
繰り返しますが、これは可能な限り単純にして、単位円 でのみ機能するようにします。半径1の円です。これは、手の長さがすべて1であることを意味しますが、明るい面では、数学が飲み込みやすくなります。
func TestSecondHandVector(t *testing.T) {
cases := []struct {
time time.Time
point Point
}{
{simpleTime(0, 0, 30), Point{0, -1}},
}
for _, c := range cases {
t.Run(testName(c.time), func(t *testing.T) {
got := secondHandPoint(c.time)
if got != c.point {
t.Fatalf("Wanted %v Point, but got %v", c.point, got)
}
})
}
}
# github.com/gypsydave5/learn-go-with-tests/math/v4/clockface [github.com/gypsydave5/learn-go-with-tests/math/v4/clockface.test]
./clockface_test.go:40:11: undefined: secondHandPoint
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface [build failed]
func secondHandPoint(t time.Time) Point {
return Point{}
}
--- FAIL: TestSecondHandPoint (0.00s)
--- FAIL: TestSecondHandPoint/00:00:30 (0.00s)
clockface_test.go:42: Wanted {0 -1} Point, but got {0 0}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.010s
func secondHandPoint(t time.Time) Point {
return Point{0, -1}
}
PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
func TestSecondHandPoint(t *testing.T) {
cases := []struct {
time time.Time
point Point
}{
{simpleTime(0, 0, 30), Point{0, -1}},
{simpleTime(0, 0, 45), Point{-1, 0}},
}
for _, c := range cases {
t.Run(testName(c.time), func(t *testing.T) {
got := secondHandPoint(c.time)
if got != c.point {
t.Fatalf("Wanted %v Point, but got %v", c.point, got)
}
})
}
}
--- FAIL: TestSecondHandPoint (0.00s)
--- FAIL: TestSecondHandPoint/00:00:45 (0.00s)
clockface_test.go:43: Wanted {-1 0} Point, but got {0 -1}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.006s
ユニットサークルの画像を覚えていますか?

ここで、XとYを生成する方程式が必要です。秒に書きましょう。
func secondHandPoint(t time.Time) Point {
angle := secondsInRadians(t)
x := math.Sin(angle)
y := math.Cos(angle)
return Point{x, y}
}
これなら、いけるでしょうか。
--- FAIL: TestSecondHandPoint (0.00s)
--- FAIL: TestSecondHandPoint/00:00:30 (0.00s)
clockface_test.go:43: Wanted {0 -1} Point, but got {1.2246467991473515e-16 -1}
--- FAIL: TestSecondHandPoint/00:00:45 (0.00s)
clockface_test.go:43: Wanted {-1 0} Point, but got {-1 -1.8369701987210272e-16}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
待って。。もう一度、フロートにもう一度呪われているようです。 予期しない数値は両方とも、小数点以下16桁で「無限」 infinitesimal のようです。 したがって、ここでも、精度を上げるか、ほぼ同じであると言っ て私たちの生活を続けるかを選択できます。
これらの角度の精度を向上させる1つのオプションは、
math/big
パッケージの合理的なタイプRat
を使用することです。 しかし、目的が月着陸ではなくSVGを描画することであることを考えると、少しぼやけて暮らせると思います。func TestSecondHandPoint(t *testing.T) {
cases := []struct {
time time.Time
point Point
}{
{simpleTime(0, 0, 30), Point{0, -1}},
{simpleTime(0, 0, 45), Point{-1, 0}},
}
for _, c := range cases {
t.Run(testName(c.time), func(t *testing.T) {
got := secondHandPoint(c.time)
if !roughlyEqualPoint(got, c.point) {
t.Fatalf("Wanted %v Point, but got %v", c.point, got)
}
})
}
}
func roughlyEqualFloat64(a, b float64) bool {
const equalityThreshold = 1e-7
return math.Abs(a-b) < equalityThreshold
}
func roughlyEqualPoint(a, b Point) bool {
return roughlyEqualFloat64(a.X, b.X) &&
roughlyEqualFloat64(a.Y, b.Y)
}
2つの
Points
間のおおよその等価性を定義する2つの関数を定義しました。- X要素とY要素が互いに0.0000001以内にある場合に機能します。それはまだかなり正確です。
そして今、私たちはテストに成功します。
PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v4/clockface 0.007s
これでもかなり満足しています。
まあ、 new と言っても完全に正確というわけではありません。 実際にできることは、受け入れテストに合格 することです!それがどのように見えるかを思い出してみましょう
func TestSecondHandAt30Seconds(t *testing.T) {
tm := time.Date(1337, time.January, 1, 0, 0, 30, 0, time.UTC)
want := clockface.Point{X: 150, Y: 150 + 90}
got := clockface.SecondHand(tm)
if got != want {
t.Errorf("Got %v, wanted %v", got, want)
}
}
--- FAIL: TestSecondHandAt30Seconds (0.00s)
clockface_acceptance_test.go:28: Got {150 60}, wanted {150 240}
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s
単位ベクトルをSVG上の点に変換するには、3つのことを行う必要があります。
- 1.長さに合わせます
- 2.SVGに原点があるSVGを考慮するため、X軸上で反転します左上隅
- 3.それを正しい位置に移動します(それが(150, 150))
楽しい時間ですね!
// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand(t time.Time) Point {
p := secondHandPoint(t)
p = Point{p.X * 90, p.Y * 90} // scale
p = Point{p.X, -p.Y} // flip
p = Point{p.X + 150, p.Y + 150} // translate
return p
}
正確にその順序でスケーリング、反転、変換されます。やったー!
PASS
ok github.com/gypsydave5/learn-go-with-tests/math/v5/clockface 0.007s
ここに定数として取り出されるべきいくつかの魔法の数字があるので、それをやってみましょう
const secondHandLength = 90
const clockCentreX = 150
const clockCentreY = 150
// SecondHand is the unit vector of the second hand of an analogue clock at time `t`
// represented as a Point.
func SecondHand(t time.Time) Point {
p := secondHandPoint(t)
p = Point{p.X * secondHandLength, p.Y * secondHandLength}
p = Point{p.X, -p.Y}
p = Point{p.X + clockCentreX, p.Y + clockCentreY} //translate
return p
}
さて...とにかく秒針...
これをやってみましょう! そこに座って人々を魅了するた めに世界に出て行くのを待っているだけで、価値を提供しないよりも悪いことはありません。秒針を描いてみよう!
メインの
clockface
パッケージディレクトリの下に、(confusingly)、clockface
という新しいディレクトリを追加します。そこにSVGをビルドするバイナリを作成するmain
パッケージを置きます。├── clockface
│ └── main.go
├── clockface.go
├── clockface_acceptance_test.go
└── clockface_test.go
main.go
の中package main
import (
"fmt"
"io"
"os"
"time"
"github.com/gypsydave5/learn-go-with-tests/math/v6/clockface"
)
func main() {
t := time.Now()
sh := clockface.SecondHand(t)
io.WriteString(os.Stdout, svgStart)
io.WriteString(os.Stdout, bezel)
io.WriteString(os.Stdout, secondHandTag(sh))
io.WriteString(os.Stdout, svgEnd)
}
func secondHandTag(p clockface.Point) string {
return fmt.Sprintf(`<line x1="150" y1="150" x2="%f" y2="%f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`, p.X, p.Y)
}
const svgStart = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 300 300"
version="2.0">`
const bezel = `<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>`
const svgEnd = `</svg>`
ああ、少年よ、私はこの混乱の美しいコードのために賞を獲得しようとしているわけではありません。 しかし、これは仕事をします。これは
os.Stdout
に SVG を書き出しています。一度に一文字ずつ。これを構築すると
go build
そしてそれを実行し、出力をファイルに送信します
./clockface > clock.svg
私たちは次のようなものを見るはずです
秒針のみの時計
これは臭い。まあ、それはまったく悪臭はしませんが、私はそれについて満足していません。
- 1.その
SecondHand
関数全体は、SVGであることに結びついています...SVGについて言及するか、実際にSVGを作成する... - 2.... 同時に、私はSVGコードをテストしていません。
ええ、私は失敗したと思います。これは間違っていると感じます。 よりSVG中心のテストで回復してみましょう。
私たちのオプションは何ですか? さて、
SVGWriter
から吐き出される文字に、特定の時間に予想されるSVGタグのようなものが含まれていることをテストしてみることができます。例えば:func TestSVGWriterAtMidnight(t *testing.T) {
tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)
var b strings.Builder
clockface.SVGWriter(&b, tm)
got := b.String()
want := `<line x1="150" y1="150" x2="150" y2="60"`
if !strings.Contains(got, want) {
t.Errorf("Expected to find the second hand %v, in the SVG output %v", want, got)
}
}
しかし、これは本当に改善されたのでしょうか?
有効なSVGを生成しなかった場合でも(パスが出力に文字列が表示されることをテストするだけなので)成功するだけでなく、その文字列に最小の重要でない変更を加えた場合も失敗します。たとえば、属性の間にスペースを追加します。
最大のにおいは、実際には、データ構造(XML)を一連の文字としての(文字列としての)表示でテストすることです。これは never 、 ever です。これは、上で概説したのと同じような問題を引き起こすためです。脆弱すぎて感度が不十分なテストです。間違ったことをテストするテスト!
したがって、唯一の解決策は、出力をXMLとしてテストすることです。そのためには、それを解析する必要があります。
したがって、XMLを非整列化するための構造体が必要になります。 すべてのノードと属性の正しい名前と正しい構造の書き方を検討するのに少し時間を費やすことができましたが、幸い、誰かが
zek
そのハードワークのすべてを自動化するプログラム。 さらに良いことに、https://www.onlinetool.io/xmltogo/にオンラインバージョンがあります。ファイルの上部から1つのボックスにSVGを貼り付けるだけです。- 飛び出します
type Svg struct {
XMLName xml.Name `xml:"svg"`
Text string `xml:",chardata"`
Xmlns string `xml:"xmlns,attr"`
Width string `xml:"width,attr"`
Height string `xml:"height,attr"`
ViewBox string `xml:"viewBox,attr"`
Version string `xml:"version,attr"`
Circle struct {
Text string `xml:",chardata"`
Cx string `xml:"cx,attr"`
Cy string `xml:"cy,attr"`
R string `xml:"r,attr"`
Style string `xml:"style,attr"`
} `xml:"circle"`
Line []struct {
Text string `xml:",chardata"`
X1 string `xml:"x1,attr"`
Y1 string `xml:"y1,attr"`
X2 string `xml:"x2,attr"`
Y2 string `xml:"y2,attr"`
Style string `xml:"style,attr"`
} `xml:"line"`
}
(構造体の名前を
SVG
に変更するなど)必要がある場合は、これを調整できますが、最初から十分です。func TestSVGWriterAtMidnight(t *testing.T) {
tm := time.Date(1337, time.January, 1, 0, 0, 0, 0, time.UTC)
b := bytes.Buffer{}
clockface.SVGWriter(&b, tm)
svg := Svg{}
xml.Unmarshal(b.Bytes(), &svg)
x2 := "150"
y2 := "60"
for _, line := range svg.Line {
if line.X2 == x2 && line.Y2 == y2 {
return
}
}
t.Errorf("Expected to find the second hand with x2 of %+v and y2 of %+v, in the SVG output %v", x2, y2, b.String())
}
clockface.SVGWriter
の出力をbytes.Buffer
に書き込み、次にUnmarshall
をSvg
に書き込みます。次に、Svg
内の各Line
を見て、それらのいずれかが期待されるX2
およびY2
値を持っているかどうかを確認します。 一致した場合、早期に(テストに合格)します。そうでない場合、(うまくいけば)情報メッセージで失敗します。# github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface_test [github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface.test]
./clockface_acceptance_test.go:41:2: undefined: clockface.SVGWriter
FAIL github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface [build failed]
その
SVGWriter
を書くほうがいいようです...package clockface
import (
"fmt"
"io"
"time"
)
const (
secondHandLength = 90
clockCentreX = 150
clockCentreY = 150
)
//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w
func SVGWriter(w io.Writer, t time.Time) {
io.WriteString(w, svgStart)
io.WriteString(w, bezel)
secondHand(w, t)
io.WriteString(w, svgEnd)
}
func secondHand(w io.Writer, t time.Time) {
p := secondHandPoint(t)
p = Point{p.X * secondHandLength, p.Y * secondHandLength} // scale
p = Point{p.X, -p.Y} // flip
p = Point{p.X + clockCentreX, p.Y + clockCentreY} // translate
fmt.Fprintf(w, `<line x1="150" y1="150" x2="%f" y2="%f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`, p.X, p.Y)
}
const svgStart = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 300 300"
version="2.0">`
const bezel = `<circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/>`
const svgEnd = `</svg>`
最も美しいSVGの書き方ですか?いいえ。でもうまくいけば、うまくいきます...
--- FAIL: TestSVGWriterAtMidnight (0.00s)
clockface_acceptance_test.go:56: Expected to find the second hand with x2 of 150 and y2 of 60, in the SVG output <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 300 300"
version="2.0"><circle cx="150" cy="150" r="100" style="fill:#fff;stroke:#000;stroke-width:5px;"/><line x1="150" y1="150" x2="150.000000" y2="60.000000" style="fill:none;stroke:#f00;stroke-width:3px;"/></svg>
FAIL
exit status 1
FAIL github.com/gypsydave5/learn-go-with-tests/math/v7b/clockface 0.008s
おっと!
%f
フォーマットディレクティブは、座標をデフォルトの精度レベル(小数点以下6桁)に出力します。座標に期待する精度のレベルを明示する必要があります。小数点第3位としましょう。s := fmt.Sprintf(`<line x1="150" y1="150" x2="%.3f" y2="%.3f" style="fill:none;stroke:#f00;stroke-width:3px;"/>`, p.X, p.Y)
テストでの期待を更新した後