How do i apply unit testing to check leap year with golang?

huangapple go评论82阅读模式
英文:

How do i apply unit testing to check leap year with golang?

问题

我想进行单元测试,并检查日期是否为闰年。在这种情况下,我该如何应用单元测试?我不知道如何处理这个问题。

我已经创建了我的函数,并且它运行良好。我还有一个包含人员列表和他们的生日日期的JSON文件!

希望能得到一些建议或意见,关于如何解决这个问题!谢谢!

BD.go:

package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"time"
)

// Users struct which contains
// an array of users
type Users struct {
	Users []User `json:"users"`
}

// User struct which contains a name
// a type and a list of social links
type User struct {
	Firstname  string `json:"fname"`
	Secondname string `json:"lname"`
	Date       string `json:"date"`
}

var users Users
var user User

func Birthday() {
	// Open our jsonFile
	jsonFile, err := os.Open("users.json")
	// if we os.Open returns an error then handle it
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println("Successfully Opened users.json")
	// defer the closing of our jsonFile so that we can parse it later on
	defer jsonFile.Close()

	// read our opened xmlFile as a byte array.
	byteValue, _ := ioutil.ReadAll(jsonFile)

	// we initialize our Users array
	// we unmarshal our byteArray which contains our
	// jsonFile's content into 'users' which we defined above
	json.Unmarshal(byteValue, &users)
	IsLeapYear("users.json")
}

func IsLeapYear(user string) bool {
	// we iterate through every user within our users array and
	// print out the user Type, their name
	for i := 0; i < len(users.Users); i++ {
		date, err := time.Parse("2006/01/02", users.Users[i].Date)
		if err != nil {
			date, err = time.Parse("2006-01-02", users.Users[i].Date)
			// date, err = time.Parse("2006 01 02", users.Users[i].Date)
			if err != nil {
				log.Fatal("unsupported date format:", err)
			}
		}

		// check if the date is a leap year, ex: 29 is not a leap year but 28th is !

		if date.Day()%400 == 0 || (date.Day()%4 == 0 && date.Day()%100 != 0) {
			fmt.Println("User First Name: " + users.Users[i].Firstname)
			fmt.Println("User Second Name: " + users.Users[i].Secondname)
			fmt.Println("User Date: " + users.Users[i].Date)
			fmt.Println(users.Users[i].Date, "is a Leap Year ✨ ✨ ✨")

			// checking if the date.day matches today's date
			if date.Day() == time.Now().Day() {
				fmt.Println("User First Name: " + users.Users[i].Firstname)
				fmt.Println("User Date: " + users.Users[i].Date)
				fmt.Println("TODAY IS YOUR BIRTHDAY, Happy birthday !!!🎉 🎉 🎰 🎰 ")

				// not ur birthday today because the date in the json doesn't match todays date
			} else {

				fmt.Println("TODAY IS NOT YOUR BIRTHDAY..!!!")

			}

		} else {
			fmt.Println("User First Name: " + users.Users[i].Firstname)
			fmt.Println("User Second Name: " + users.Users[i].Secondname)
			fmt.Println("User Date: " + users.Users[i].Date)
			fmt.Println(users.Users[i].Date, " is Not a Leap Year 🅰️ 🅰️ 🅰️ ")
		}

	}

	return false
}

func main() {
	Birthday()

}

bd_test.go:

package main

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

//main_test.go
type dateTest struct {
	date   time.Time
	expect bool
}

var dateTests = []dateTest{
	{"2021-07-28", true},
	{time.Date(2021,9,10), false},
}

func TestIsLeapYear(t *testing.T) {
	for _, tc := range dateTests {
		result := IsLeapYear(tc.date)
		assert.Equal(t, tc.expect, result)
	}
}

users.JSON:

{
    "users": [
      {
        "Fname": "Johnny",
        "Lname":"mane",
        "date":"1982/01/08"
      },
      {
        "Fname": "Wayne",
        "Lname":"Bruce",
        "date":"1965/01/30"
      },
      {
        "Fname": "Gaga",
        "Lname":"Lady",
        "date":"1986/03/08"
      },
      {
        "Fname": "radio",
        "Lname":"head",
        "date":"1988/02/29"
      },
      {
        "Fname": "Mario",
        "Lname":"torres",
        "date":"1996/09/04"
      },
      
      {
        "Fname": "robert",
        "Lname":"Alex",
        "date":"1991/12/05"
      },
      {
        "Fname": "Julia",
        "Lname":"sevak",
        "date":"1991-03-28"

      },
      {
        "Fname": "feb",
        "Lname":"robert",
        "date":"1995-05-24"

      },
      {
        "Fname": "Liam",
        "Lname":"Noah",
        "date":"2002-10-04"

      },
      {
        "Fname": "alex",
        "Lname":"sam",
        "date":"2021/10/21"

      }
      
      }

    ]
  }
英文:

I want to do unit testing and check if dates are leap year or not, how do i apply unit testing in this case, i don’t t know how to go about it in this situation?

I have already created my function and it works fine, i also have a JSON file that contains list of people and their Birthday dates !

Would appreciate some feedback or advice on how to tackle this problem ! Thanks

BD.go:

 package main
import (
&quot;encoding/json&quot;
&quot;fmt&quot;
&quot;io/ioutil&quot;
&quot;log&quot;
&quot;os&quot;
&quot;time&quot;
)
// Users struct which contains
// an array of users
type Users struct {
Users []User `json:&quot;users&quot;`
}
// User struct which contains a name
// a type and a list of social links
type User struct {
Firstname  string `json:&quot;fname&quot;`
Secondname string `json:&quot;lname&quot;`
Date       string `json:&quot;date&quot;`
}
var users Users
var user User
func Birthday() {
// Open our jsonFile
jsonFile, err := os.Open(&quot;users.json&quot;)
// if we os.Open returns an error then handle it
if err != nil {
fmt.Println(err)
}
fmt.Println(&quot;Successfully Opened users.json&quot;)
// defer the closing of our jsonFile so that we can parse it later on
defer jsonFile.Close()
// read our opened xmlFile as a byte array.
byteValue, _ := ioutil.ReadAll(jsonFile)
// we initialize our Users array
// we unmarshal our byteArray which contains our
// jsonFile&#39;s content into &#39;users&#39; which we defined above
json.Unmarshal(byteValue, &amp;users)
IsLeapYear(&quot;users.json&quot;)
}
func IsLeapYear(user string) bool {
// we iterate through every user within our users array and
// print out the user Type, their name
for i := 0; i &lt; len(users.Users); i++ {
date, err := time.Parse(&quot;2006/01/02&quot;, users.Users[i].Date)
if err != nil {
date, err = time.Parse(&quot;2006-01-02&quot;, users.Users[i].Date)
// date, err = time.Parse(&quot;2006 01 02&quot;, users.Users[i].Date)
if err != nil {
log.Fatal(&quot;unsupported date format:&quot;, err)
}
}
// check if the date is a leap year, ex: 29 is not a leap year but 28th is !
if date.Day()%400 == 0 || (date.Day()%4 == 0 &amp;&amp; date.Day()%100 != 0) {
fmt.Println(&quot;User First Name: &quot; + users.Users[i].Firstname)
fmt.Println(&quot;User Second Name: &quot; + users.Users[i].Secondname)
fmt.Println(&quot;User Date: &quot; + users.Users[i].Date)
fmt.Println(users.Users[i].Date, &quot;is a Leap Year ✨ ✨ ✨&quot;)
// checking if the date.day matches today&#39;s date
if date.Day() == time.Now().Day() {
fmt.Println(&quot;User First Name: &quot; + users.Users[i].Firstname)
fmt.Println(&quot;User Date: &quot; + users.Users[i].Date)
fmt.Println(&quot;TODAY IS YOUR BIRTHDAY, Happy birthday !!!&#127881; &#127881; &#127870; &#127870; &quot;)
// not ur birthday today because the date in the json doesn&#39;t match todays date
} else {
fmt.Println(&quot;TODAY IS NOT YOUR BIRTHDAY..!!!&quot;)
}
} else {
fmt.Println(&quot;User First Name: &quot; + users.Users[i].Firstname)
fmt.Println(&quot;User Second Name: &quot; + users.Users[i].Secondname)
fmt.Println(&quot;User Date: &quot; + users.Users[i].Date)
fmt.Println(users.Users[i].Date, &quot; is Not a Leap Year &#128165; &#128165; &#128165; &quot;)
}
}
return false
}
func main() {
Birthday()
}

bd_test.go:

package main

import (
&quot;testing&quot;
&quot;time&quot;
&quot;github.com/stretchr/testify/assert&quot;
)
//main_test.go
type dateTest struct {
date   time.Time
expect bool
}
var dateTests = []dateTest{
{&quot;2021-07-28&quot;, true},
{time.Date(2021,9,10), false},
}
func TestIsLeapYear(t *testing.T) {
for _, tc := range dateTests {
result := IsLeapYear(tc.date)
assert.Equal(t, tc.expect, result)
}
}

users.JSON:

{
&quot;users&quot;: [
{
&quot;Fname&quot;: &quot;Johnny&quot;,
&quot;Lname&quot;:&quot;mane&quot;,
&quot;date&quot;:&quot;1982/01/08&quot;
},
{
&quot;Fname&quot;: &quot;Wayne&quot;,
&quot;Lname&quot;:&quot;Bruce&quot;,
&quot;date&quot;:&quot;1965/01/30&quot;
},
{
&quot;Fname&quot;: &quot;Gaga&quot;,
&quot;Lname&quot;:&quot;Lady&quot;,
&quot;date&quot;:&quot;1986/03/08&quot;
},
{
&quot;Fname&quot;: &quot;radio&quot;,
&quot;Lname&quot;:&quot;head&quot;,
&quot;date&quot;:&quot;1988/02/29&quot;
},
{
&quot;Fname&quot;: &quot;Mario&quot;,
&quot;Lname&quot;:&quot;torres&quot;,
&quot;date&quot;:&quot;1996/09/04&quot;
},
{
&quot;Fname&quot;: &quot;robert&quot;,
&quot;Lname&quot;:&quot;Alex&quot;,
&quot;date&quot;:&quot;1991/12/05&quot;
},
{
&quot;Fname&quot;: &quot;Julia&quot;,
&quot;Lname&quot;:&quot;sevak&quot;,
&quot;date&quot;:&quot;1991-03-28&quot;
},
{
&quot;Fname&quot;: &quot;feb&quot;,
&quot;Lname&quot;:&quot;robert&quot;,
&quot;date&quot;:&quot;1995-05-24&quot;
},
{
&quot;Fname&quot;: &quot;Liam&quot;,
&quot;Lname&quot;:&quot;Noah&quot;,
&quot;date&quot;:&quot;2002-10-04&quot;
},
{
&quot;Fname&quot;: &quot;alex&quot;,
&quot;Lname&quot;:&quot;sam&quot;,
&quot;date&quot;:&quot;2021/10/21&quot;
}
}
]
}

答案1

得分: 2

你的代码需要进行一些重构才能进行测试。目前你无法真正测试代码,因为代码中的函数没有返回任何内容。在单元测试中,你调用一个函数并验证其输出(一般而言)。

为了使你的代码可测试,你需要将代码的某些部分重构为单独的函数。我将以闰年为例给你展示一个示例:

//main.go
func IsLeapYear(date time.Time) bool {
  if date.Year() % 400 == 0 {
    return true
  }
  return date.Year()%4 == 0 && date.Year()%100 != 0
}

在你的测试文件中:

//main_test.go
type dateTest struct {
  date time.Time
  expect bool
}

var dateTests = []dateTest{
  // 你的测试数据
}

func TestIsLeapYear(t *testing.T){
  for _, tc := range dateTests {
    result := IsLeapYear(tc.data)
    assert.Equal(t, tc.expect, result)
  }
}

英文:

You code would require some refactoring to make it testable. Currently you can't really test the code, as the functions in your code do not return anything. In a unit test you call a function and validate it's output (generally speaking).

So to make your code testable you'll have to restructure certain parts of the code in separate functions. I'll show you an example for the leap year:

//main.go
func IsLeapYear(date time.Time) bool {
  if date.Year() % 400 == 0 {
    return true
  }
  return date.Year()%4 == 0 &amp;&amp; date.Year()%100 != 0
}

in your test file:

//main_test.go
type dateTest struct {
  date time.Time
  expect bool
}

var dateTests = []dateTest{
  // your test data
}

func TestIsLeapYear(t *testing.T){
  for _, tc := range dateTests {
    result := IsLeapYear(tc.data)
    assert.Equal(t, tc.expect, result)
  }
}

答案2

得分: 1

关于你的更新代码:

我将从我对你的单个最重要的建议开始:停止忽略错误!检查错误对于调试你的Go代码和运行程序非常重要。请记住,与许多高级语言(如JavaScript)不同,Go语言中没有异常。你检查错误返回值是捕捉这些错误的唯一机会

例如:

json.Unmarshal(byteValue, &users)

这行代码如果json无效会返回一个错误。你应该检查它。查阅你使用的每个库函数的文档(如果你没有记住的话),确保你处理了这些错误信息。

好的,让我们来看看你传递给IsLeapYear的内容:

jsonFile, err := os.Open("users.json")
...
IsLeapYear("users.json")

当你到达IsLeapYear时,你已经不需要文件名了。你已经打开了该文件并将其值解组为Users结构体。所以你肯定不需要将文件名传递给它。

让我强调一下Pim向你展示的你没有完全遵循的内容:

func IsLeapYear(date time.Time) bool

Pim的IsLeapYear检查一个日期是很重要的。你的测试用例是需要逐个检查的单个日期。但是你的IsLeapYear遍历了一个整个日期列表。你不能将特定的日期传递给它进行检查。你的闰年函数也期望接收多个日期,但你只返回一个bool值,所以这个单一的返回值不可能告诉你每个传递的多个日期的闰年检查结果。

按照Pim对IsLeapYear的建议进行操作——它应该只有一个参数,类型为time.Time。名字和姓氏对于IsLeapYear来说并不相关,所以不需要传递它们。IsLeapYear不需要解析日期——日期的格式与你在json中存储数据的方式无关,也与日期是否为闰年无关。

目标是编写10-20行的函数。

我提出这个建议的原因是,长函数几乎总是做了太多的事情,这表明你需要重构函数,以更好地匹配你的算法的组成部分。让我们将你的代码分解为伪代码,以更好地反映你的算法:

  • 从json文件中读取用户
  • 对于每个用户:
    • 解析其日期的预期格式
    • 如果是闰年生日:
      • 打印用户信息
      • 打印闰年消息
      • 如果生日是今天,则打印用户信息和生日消息
      • 否则打印非生日消息
    • 否则打印用户信息和非生日消息

从中我们可以确定一些我们可能编写的函数:

  • func ReadUsersFromJson(filename string) (*Users, error)
  • func ParseUserDateString(date string) (time.Time, error)
  • func IsLeapYear(date time.Time) bool
  • func PrintUserInfo(u User)
  • func IsTodaysDate(date time.Time) bool

这些函数都很简单明了,只做一件事。每个函数都可以逐个进行测试。你的一般测试方法很常见:编写一个完整的输入列表(以及期望的输出——你漏掉了这部分),然后迭代遍历列表,将每个输入传递给被测试的函数,并验证其结果是否符合预期。

对于ReadUsersFromJson,你可以在tests/目录下准备一些json文件进行测试。你可以使用有效和无效的json测试成功和错误的情况。

类似地,ParseUserDateString的测试数据可能如下所示:

type ParseUserDateStringTestData struct {
  input        string
  expected     time.Time
  expectedErrMsg string
}

然后你可以测试成功和失败的情况。其他函数也可以按照类似的方式进行测试。一旦所有的函数都编写和测试完毕,只需要在Birthday()函数中将所有的函数组合在一起即可。

func Birthday() {
   users, err := ReadUsersFromJson("users.json")
   if err != nil {
     panic(fmt.Errorf("Failed to read from Json: %w", err))
   }
   for _, user := range users.Users {
      if d, err := ParseUserDateString(user.Date); err != nil {
         panic(fmt.Errorf("Date %s could not be parsed: %w", user.Date, err))
      } else if IsLeapYear(d) {
         PrintUserInfo(user)
         fmt.Printf("Leap year message!")
         if IsTodaysDate(d) {
           fmt.Printf("birthday message!")
         } else {
           fmt.Printf("Not birthday message!")
         }
      } else {
         fmt.Printf("Not leap year message!")
      }
   }
}

这段代码刚好有20行,而且非常清晰地展示了它的功能。你会注意到,由于我编写了PrintUserInfo函数,它消除了Birthday中的大量重复代码。这种重复使得函数更难阅读和管理。

如果你按照我提出的这些函数编写并测试所有的函数,你的程序应该会很好地组合起来。请注意,main()Birthday()不需要编写来测试其他函数。在编写函数时进行测试是一个好主意,这样就不会积压大量的测试,并且可以对你编写的代码产生信心。

举个例子:

if date.Day() == time.Now().Day() {

我不认为它做的是你认为它在做的事情。Day()实际上是月份的天数,所以你实际上只是在说,日期的天数与今天的日期的天数相同吗?任何一个月的29号都与2月29号具有相同的Day()。通过测试可以证明这一点,然后你可以在以后依赖这个功能。

英文:

Regarding your updated code:

I'll begin with the single most important piece of advice I have for you: stop ignoring errors! Checking errors is absolutely critical to debugging your Go code and also operating your program. Remember that unlike many higher level languages like Javascript, there are no exceptions in Go. Your checking of error returns is the only chance you have to catch those errors.

For example:

    json.Unmarshal(byteValue, &amp;users)

That line returns an error if the json is invalid. You should check it. Look up the documentation on each of the library functions you use that you haven't memorized - I do - and make sure you are consuming those error messages.

Okay, let's take a look at what you're passing to IsLeapYear:

    jsonFile, err := os.Open(&quot;users.json&quot;)
...
IsLeapYear(&quot;users.json&quot;)

by the time you get to IsLeapYear you're past the point where you need the file name. You've already opened that file and unmarshaled its value to a Users struct. So you certainly don't need to pass it the file name.

Let me highlight something Pim showed you that you didn't follow faithfully:

func IsLeapYear(date time.Time) bool

The fact that Pim's IsLeapYear checks one date is important. Your test cases are individual dates that need to be individually checked. But your IsLeapYear iterates over a whole list of dates. You can't feed specific dates to be checked to that. Your leap year also expects to receive multiple dates, but you only return one bool, so that single return value can't possibly tell you the result of the leap year checks on each of the multiple dates you may have passed.

Follow Pim's suggestion for IsLeapYear - it should have exactly one argument and it should be a time.Time. First and Last name is not relevant to IsLeapYear so it doesn't need to be passed. IsLeapYear does not need to parse dates - their format is incidental to the way you stored your data in json, and is not relevant to whether the date they represent is a Leap Year.

> Aim for 10-20 line functions.

The reason I make this suggestion, is because long functions almost always are doing too much and it's a sign that you need to refactor your functions to better match the components of your algorithm. Let's break up your code into pseudocode to better reflect your algorithm:

<ul>
<li>Read Users from json file</li>
<li>For each user: <ul>
<li>parse its date from the expected format
<li>if it is a leap year birthday
<ul>
<li>print user info
<li>print leap year message
<li>if birthdate is today, print user info and birthday message
<li>else print not birthday message
</ul>
</li>
<li>else print user info and not birthday message
</ul>
</ul>

From that we can identify some functions we might write:

  • func ReadUsersFromJson(filename string) (*Users, err)
  • func ParseUserDateString(date string) (time.Time, error)
  • func IsLeapYear(date time.Time) bool
  • func PrintUserInfo(u User)
  • func IsTodaysDate(date time.Time) bool

Each of these functions is pretty straight forward and short, and only does one thing. Each of them could be tested in turn. Your general approach to testing is common: write a whole list of inputs ( and expeected outputs - you missed that part) , then iterate over the list, passing each of the inputs to the function being tested, and verifying that its result was what you expected.

For ReadUsersFromJson you could have a tests/ directory with some json files you test against. You could test both success and error cases with valid and invalid json.

Similarly, ParseUserDateString test data might look something like:

struct ParseUserDateStringTestData {
input string
expected time.Time
exp_err_msg string
}

Then you can test both success and failure cases there. And so on for the rest of the functions.

Once all the functions are written and tested, it's just a question of assembling all the functions together in Birthday().

func Birthday() {
users, err := ReadUsersFromJson(&quot;users.json&quot;)
if err != nil {
panic(fmt.Errorf(&quot;Failed to read from Json: %w&quot;, err))
}
for _, user := range users.Users {
if d, err := ParseUserDateString(user.Date); err != nil {
panic(fmt.Errorf(&quot;Date %s could not be parsed: %w&quot;, user.Date, err))
} else if IsLeapYear(d) {
PrintUserInfo(user)
fmt.Printf(&quot;Leap year message!&quot;)
if IsTodaysDate(d) {
fmt.Printf(&quot;birthday message!&quot;)
} else {
fmt.Printf(&quot;Not birthday message!&quot;)
}
} else {
fmt.Printf(&quot;Not leap year message!&quot;)
}
}
}

It just so happens to come in at 20 lines, and it also is quite clear what it's doing. You'll notice that since I wrote PrintUserInfo it removed a bunch of duplication in Birthday. This duplication makes the function harder to read and manage of course.

If you write functions like the ones I lay out, and test all of them, your program should come together nicely. Notice that main() or Birthday() do not need to be written to test the other functions. It's a good idea to test your functions as you write them, so as not to end up with a huge backlog of tests, and to instill confidence in the code you've written.

Case in point:

            if date.Day() == time.Now().Day() {

I don't think that's doing what you think it's doing. Day() is actually the day of the month, so you're actually just saying, is the day of the date the same day of the month as today's day? The 29th of any month would be the same Day() as the 29th of February. Testing would prove whether or not this was the case, then you could rely on that functionality later.

huangapple
  • 本文由 发表于 2021年10月22日 16:21:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/69673616.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定