日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

序幕

這篇文章是集成測(cè)試系列兩個(gè)部分中的第二部分。你可以先讀 Go 語言中的集成測(cè)試:第一部分 - 用 Docker 執(zhí)行測(cè)試。本文中的示例可以從 代碼倉庫[1] 獲取。

簡介

“比起測(cè)試行為,設(shè)計(jì)測(cè)試行為是已知的最好的錯(cuò)誤預(yù)防程序之一。” —— Boris Beizer

在執(zhí)行集成測(cè)試之前,必須正確配置該測(cè)試相關(guān)的外部系統(tǒng)。否則,測(cè)試結(jié)果是無效和不可靠的。例如,數(shù)據(jù)庫需要有定義好的數(shù)據(jù),這些數(shù)據(jù)對(duì)于要測(cè)試的行為是正確的。測(cè)試期間更改的數(shù)據(jù)需要進(jìn)行驗(yàn)證,尤其是如果要求更改的數(shù)據(jù)對(duì)于后續(xù)測(cè)試而言是準(zhǔn)確的時(shí)侯。

Go 測(cè)試工具提供了有在執(zhí)行測(cè)試函數(shù)前執(zhí)行代碼的能力,使用叫做 TestMain 的入口函數(shù)實(shí)現(xiàn)。它類似于 Go 應(yīng)用程序的 Main 函數(shù)。有了 TestMain 函數(shù),我們可以在執(zhí)行測(cè)試之前做其他系統(tǒng)配置,比如數(shù)據(jù)庫連接之類的。在本文中,我將分享如何使用它 TestMain 來配置和連接 Postgres 數(shù)據(jù)庫,以及如何針對(duì)該數(shù)據(jù)庫編寫和運(yùn)行測(cè)試。

填充初始數(shù)據(jù)

為了填充數(shù)據(jù)庫,需要定義數(shù)據(jù)并將其放置在測(cè)試工具可以訪問的位置。一種常見的方法是定義一個(gè) SQL 文件,該文件是項(xiàng)目的一部分,并且包含所有需要執(zhí)行的 SQL 命令。另一種方法是將 SQL 命令存儲(chǔ)在代碼內(nèi)部的常量中。不同于這兩種方法,我將只使用 Go 語言實(shí)現(xiàn)來解決此問題。

通常情況下,你已將你的數(shù)據(jù)結(jié)構(gòu)定義為 Go 結(jié)構(gòu)體類型,用于數(shù)據(jù)庫通信。我將利用這些已存在的數(shù)據(jù)結(jié)構(gòu),已經(jīng)可以控制數(shù)據(jù)從數(shù)據(jù)庫中流入流出。基于已有的數(shù)據(jù)結(jié)構(gòu)聲明變量,構(gòu)造所有填充數(shù)據(jù),而無需 SQL 語句。

我喜歡這種解決方式,因?yàn)樗喕司帉懠蓽y(cè)試和驗(yàn)證數(shù)據(jù)是否能夠正確用于數(shù)據(jù)庫和應(yīng)用程序之間的通信的。不必將數(shù)據(jù)直接與 JSON 比較,就可以將數(shù)據(jù)解編為適當(dāng)?shù)念愋停缓笾苯优c為之前數(shù)據(jù)結(jié)構(gòu)定義的變量進(jìn)行比較。這不僅可以最大程度地減少測(cè)試中的語法比較錯(cuò)誤,還可以使您的測(cè)試更具可維護(hù)性、可擴(kuò)展性和可讀性。

填充數(shù)據(jù)庫

譯者注:原文為 Seeding The Database,下面部分相關(guān)功能函數(shù)就稱為種子函數(shù)

本文提到的,所有用于填充數(shù)數(shù)據(jù)庫功能函數(shù),都在 `testdb`[2] 包中。這個(gè)包僅用于測(cè)試,不用做第三方依賴。用來輔助填充測(cè)試數(shù)據(jù)庫的三個(gè)核心函數(shù)分別是:SeedLists, SeedItems, 和Truncate,如下:

這是 SeedLists 函數(shù):

代碼清單 1

func SeedLists(dbc *sqlx.DB) ([]list.List, error) {
    now := time.Now().Truncate(time.Microsecond)

    lists := []list.List{
        {
            Name:     "Grocery",
            Created:  now,
            Modified: now,
        },
        {
            Name:     "To-do",
            Created:  now,
            Modified: now,
        },
        {
            Name:     "Employees",
            Created:  now,
            Modified: now,
        },
    }

    for i := range lists {
        stmt, err := dbc.Prepare("INSERT INTO list (name, created, modified) VALUES ($1, $2, $3) RETURNING list_id;")
        if err != nil {
            return nil, errors.Wrap(err, "prepare list insertion")
        }

        row := stmt.QueryRow(lists[i].Name, lists[i].Created, lists[i].Modified)

        if err = row.Scan(&lists[i].ID); err != nil {
            if err := stmt.Close(); err != nil {
                return nil, errors.Wrap(err, "close psql statement")
            }

            return nil, errors.Wrap(err, "capture list id")
        }

        if err := stmt.Close(); err != nil {
            return nil, errors.Wrap(err, "close psql statement")
        }
    }

    return lists, nil
}

代碼清單 1 展示了 SeedLists 函數(shù)及其如何創(chuàng)建測(cè)試數(shù)據(jù)。list.List 定義了一個(gè)用于插入的數(shù)據(jù)表。然后,將測(cè)試數(shù)據(jù)插入數(shù)據(jù)庫。為了幫助將插入的數(shù)據(jù)與測(cè)試期間進(jìn)行的任何數(shù)據(jù)庫調(diào)用的結(jié)果進(jìn)行比較,測(cè)試數(shù)據(jù)集返回給調(diào)用方。

接下來,我們看看將更多測(cè)試數(shù)據(jù)插入數(shù)據(jù)庫的 SeedItems 函數(shù)。

代碼清單 2

func SeedItems(dbc *sqlx.DB, lists []list.List) ([]item.Item, error) {
    now := time.Now().Truncate(time.Microsecond)

    items := []item.Item{
        {
            ListID:   lists[0].ID, // Grocery
            Name:     "Chocolate Milk",
            Quantity: 1,
            Created:  now,
            Modified: now,
        },
        {
            ListID:   lists[0].ID, // Grocery
            Name:     "mac and Cheese",
            Quantity: 2,
            Created:  now,
            Modified: now,
        },
        {
            ListID:   lists[1].ID, // To-do
            Name:     "Write Integration Tests",
            Quantity: 1,
            Created:  now,
            Modified: now,
        },
    }

    for i := range items {
        stmt, err := dbc.Prepare("INSERT INTO item (list_id, name, quantity, created, modified) VALUES ($1, $2, $3, $4, $5) RETURNING item_id;")
        if err != nil {
            return nil, errors.Wrap(err, "prepare item insertion")
        }

        row := stmt.QueryRow(items[i].ListID, items[i].Name, items[i].Quantity, items[i].Created, items[i].Modified)

        if err = row.Scan(&items[i].ID); err != nil {
            if err := stmt.Close(); err != nil {
                return nil, errors.Wrap(err, "close psql statement")
            }

            return nil, errors.Wrap(err, "capture list id")
        }

        if err := stmt.Close(); err != nil {
            return nil, errors.Wrap(err, "close psql statement")
        }
    }

    return items, nil
}

代碼清單 2 顯示了 SeedItems 函數(shù)如何創(chuàng)建測(cè)試數(shù)據(jù)。除了使用 item.Item 數(shù)據(jù)類型,該代碼與清單 1 基本相同。testdb 包中還有一個(gè)未提到的函數(shù) Truncate。

代碼清單 3

func Truncate(dbc *sqlx.DB) error {
    stmt := "TRUNCATE TABLE list, item;"

    if _, err := dbc.Exec(stmt); err != nil {
        return errors.Wrap(err, "truncate test database tables")
    }

    return nil
}

代碼清單 3 展示了 Truncate 函數(shù)。顧名思義,它用于刪除 SeedLists 和 SeedItems 函數(shù)插入的所有數(shù)據(jù)。

使用 testing.M 創(chuàng)建 TestMain

使用便于 填充/清除 數(shù)據(jù)庫的軟件包后,該集中精力配置以運(yùn)行真正的集成測(cè)試了。Go 自帶的測(cè)試工具可以讓你在 TestMain 函數(shù)中定義需要的行為,在測(cè)試函數(shù)執(zhí)行前執(zhí)行。

代碼清單 4

func TestMain(m *testing.M) {
    os.Exit(testMain(m))
}

代碼清單 4 是 TestMain 函數(shù),它在所有集成測(cè)試之前執(zhí)行。在 23 行,叫做 testMain 的未導(dǎo)出的函數(shù)被 os.Exit 調(diào)用。這樣做是為了 testMain 可以執(zhí)行其中的延遲函數(shù),并且仍可以在 os.Exit 調(diào)用內(nèi)部設(shè)置適當(dāng)?shù)恼麛?shù)值。以下是 testMain 函數(shù)的實(shí)現(xiàn)。

代碼清單 5

func testMain(m *testing.M) int {
    dbc, err := testdb.Open()
    if err != nil {
        log.WithError(err).Info("create test database connection")
        return 1
    }
    defer dbc.Close()

    a = handlers.NewApplication(dbc)

    return m.Run()
}

在代碼清單 5 中,你可以看到 testMain 只有 8 行代碼。28 行,函數(shù)調(diào)用 testdb.Open() 開始建立數(shù)據(jù)庫連接。此調(diào)用的配置參數(shù)在 testdb 包中設(shè)置為常量。重要的是要注意,如果測(cè)試用的數(shù)據(jù)庫未運(yùn)行,調(diào)用 Opne 連接數(shù)據(jù)庫會(huì)失敗。該測(cè)試數(shù)據(jù)庫是由 docker-compose 創(chuàng)建提供的,詳細(xì)說明在本系列的第 1 部分中(單擊 這里[3] 閱讀第 1 部分)。

成功連接測(cè)試數(shù)據(jù)庫后,連接將傳遞給 handlers.NewApplication(),并且此函數(shù)的返回值用于初始化的包級(jí)變量 *handlers.Application 類型。handlers.Application 類型是這個(gè)項(xiàng)目自定義的結(jié)構(gòu)體,有用于 http.Handler 接口的字段,以簡化 Web 服務(wù)的路由以及對(duì)已創(chuàng)建的數(shù)據(jù)庫連接的引用。

現(xiàn)在,應(yīng)用程序值已初始化,可以調(diào)用 m.Run 來執(zhí)行所有測(cè)試函數(shù)。對(duì) m.Run 的調(diào)用處于阻塞狀態(tài),直到所有確定要運(yùn)行的測(cè)試函數(shù)都執(zhí)行完之后,該調(diào)用才會(huì)返回。非零退出代碼表示失敗,0 表示成功。

編寫 Web 服務(wù)的集成測(cè)試

集成測(cè)試將多個(gè)代碼單元以及所有集成服務(wù)(例如數(shù)據(jù)庫)組合在一起,并測(cè)試各個(gè)單元的功能以及各個(gè)單元之間的關(guān)系。為 Web 服務(wù)編寫集成測(cè)試通常意味著每個(gè)集成測(cè)試的所有入口點(diǎn)都是一個(gè)路由。http.Handler 接口是任何 Web 服務(wù)的必需組件,它包含的 ServeHTTP 函數(shù)使我們能夠利用應(yīng)用程序中定義的路由。

在 Web 服務(wù)的集成測(cè)試中,構(gòu)建初始化數(shù)據(jù)并且以 Go 類型返回初始數(shù)據(jù),對(duì)返回的響應(yīng)體的結(jié)構(gòu)進(jìn)行斷言非常有用。在接下來的代碼清單中,我將一個(gè)典型的 API 路由集成測(cè)試分解成幾個(gè)不同的部分。第一步是使用代碼清單 1 和代碼清單 2 中定義的種子數(shù)據(jù)。

清單 6

func Test_getItems(t *testing.T) {
    defer func() {
        if err := testdb.Truncate(a.DB); err != nil {
            t.Errorf("error truncating test database tables: %v", err)
        }
    }()

    expectedLists, err := testdb.SeedLists(a.DB)
    if err != nil {
        t.Fatalf("error seeding lists: %v", err)
    }

    expectedItems, err := testdb.SeedItems(a.DB, expectedLists)
    if err != nil {
        t.Fatalf("error seeding items: %v", err)
    }
}

在獲取種子數(shù)據(jù)失敗前,必須設(shè)置延遲函數(shù)清理數(shù)據(jù)庫,這樣,無論函數(shù)失敗與否,測(cè)試結(jié)束后保證數(shù)據(jù)庫是干凈的。然后,調(diào)用 testdb 中的種子函數(shù)(testdb.SeedLists 和 testdb.SeedItems )構(gòu)造初始數(shù)據(jù),并獲取他們的返回值作為預(yù)期值,以便在集成測(cè)試中與實(shí)際路由請(qǐng)求結(jié)果(真實(shí)值)做對(duì)比。如果這兩個(gè)種子函數(shù)中的任何一個(gè)失敗,測(cè)試就會(huì)調(diào)用 t.Fatalf 。

清單 7

// Application is the struct that contains the server handler as well as
// any references to services that the application needs.
type Application struct {
    DB      *sqlx.DB
    handler http.Handler
}

// ServeHTTP implements the http.Handler interface for the Application type.
func (a *Application) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    a.handler.ServeHTTP(w, r)
}

為了調(diào)用注冊(cè)的路由,Application 類型實(shí)現(xiàn) http.Handler 接口。http.Handler 作為 Application 的內(nèi)嵌結(jié)構(gòu)體字段,因此 Application 可以調(diào)用 http.Handler 接口實(shí)現(xiàn)的ServeHTTP 函數(shù)

清單 8

req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/list/%d/item", test.ListID), nil)
if err != nil {
   t.Errorf("error creating request: %v", err)
}

w := httptest.NewRecorder()
a.ServeHTTP(w, req)

回顧一下代碼清單 5,構(gòu)造 Application 是為了在測(cè)試中使用。ServeHTTP 函數(shù)需要兩個(gè)參數(shù):http.ResponseWriter 和 http.Request。http.NewRequest 構(gòu)造 http.Request,httptest.NewRecorder 構(gòu)造 http.ResponseRecorder——即 http.Response 。

http.NewRecorder 函數(shù)的返回 ResponseRecorder 值實(shí)現(xiàn)了 ResponseWriter 接口。調(diào)用路由請(qǐng)求后,ResponseRecorder 可以用來分析了。其中最關(guān)鍵的字段 Code 和 Body,前者是該請(qǐng)求的實(shí)際響應(yīng)碼,后者是一個(gè)指向響應(yīng)內(nèi)容的 bytes.Buffer 類型的指針。

譯者注:這里的 http.ResponseWriter 和 http.Request 實(shí)現(xiàn)了 Golang 中常見的 Writer和 Reader 接口,即 輸出 和 輸入,在 http 請(qǐng)求中即 Response 和 Request。

清單 9

if want, got := http.StatusOK, w.Code; want != got {
    t.Errorf("expected status code: %v, got status code: %v", want, got)
}

清單 9 中,實(shí)際的響應(yīng)碼和預(yù)期的響應(yīng)碼做對(duì)比。如果不同,將調(diào)用 t.Errorf,它將輸出失敗原因。

清單 10

var items []item.Item
resp := web.Response{
    Results: items,
}

if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
    t.Errorf("error decoding response body: %v", err)
}

if d := cmp.Diff(expectedItems, items); d != "" {
    t.Errorf("unexpected difference in response body:n%v", d)
}

示例中使用自定義響應(yīng)體 web.Response,使用 鍵為 results 的 JSON 字符串存儲(chǔ)路由返回信息。代碼清單 10 中聲明了一個(gè) []item.Item 類型的變量 items,用于和預(yù)期值對(duì)比。初始化 items 變量傳遞給 resp 的字段 results。接下來,items 會(huì)隨著解析路由響應(yīng)體數(shù)據(jù)到 resp 中,從而包含響應(yīng)體的數(shù)據(jù)。

google 的 go-cmp[4] 包可替代 reflect.DeepEqual ,在對(duì)比 struct,map,slice 和 array 時(shí)更安全,更易用。調(diào)用 cmp.Diff 對(duì)比清單 6 中定義的種子數(shù)據(jù)和實(shí)際響應(yīng)體中返回的數(shù)據(jù),如果不等,測(cè)試將失敗,并且將差異輸出到標(biāo)準(zhǔn)輸出(stdout)中。

測(cè)試技巧

就測(cè)試而言,最好的建議是盡早測(cè)試,并且經(jīng)常測(cè)試,而不是將測(cè)試放到開發(fā)之后考慮,而且測(cè)試應(yīng)該推動(dòng)、驅(qū)動(dòng)應(yīng)用程序的開發(fā)。這就是“測(cè)試驅(qū)動(dòng)開發(fā)(TDD)”。通常情況下,沒有隨時(shí)測(cè)試代碼。在編寫代碼時(shí),將測(cè)試的想法拋到腦后,自己(開發(fā)人員)默認(rèn)編寫的代碼是可測(cè)試的。代碼單元(通常是一個(gè)函數(shù))不管再小都能進(jìn)行測(cè)試。你的服務(wù)進(jìn)行越多測(cè)試,未知的就越少,隱藏的副作用(bug)就越少。

有了下面這些技巧,你的測(cè)試將洞察力,更易讀,更快。

表測(cè)試

表測(cè)試是一種編寫測(cè)試的方式,可以防止針對(duì)同一代碼單元的不同可測(cè)試結(jié)果重復(fù)測(cè)試斷言。以下面的求和函數(shù)為例:

清單 11

// Add takes an indefinite amount of operands and adds them together, returning
// the sum of the operation.
func Add(operands ...int) int {
    var sum int

    for _, operand := range operands {
        sum += operand
    }

    return sum
}

在測(cè)試中,我想確保函數(shù)可以處理以下情況:

  • 沒有參數(shù)(operands),應(yīng)返回 0。
  • 一個(gè)參數(shù),直接返回參數(shù)值。
  • 兩個(gè)參數(shù),返回這兩個(gè)數(shù)之和。
  • 三個(gè)參數(shù),則返回這三個(gè)數(shù)之和。

彼此獨(dú)立地編寫這些測(cè)試將導(dǎo)致重復(fù)許多相同的調(diào)用和斷言。我認(rèn)為,更好的方法是利用表測(cè)試。為了編寫表測(cè)試,必須定義一片匿名聲明的結(jié)構(gòu),其中包含我們每個(gè)測(cè)試用例的元數(shù)據(jù)。然后可以使用循環(huán)遍歷不同測(cè)試用例的這些條目,并可以對(duì)用例進(jìn)行測(cè)試和獨(dú)立運(yùn)行 t.Run。t.Run 需要兩個(gè)參數(shù),子測(cè)試函數(shù)和這個(gè)子測(cè)試函數(shù)的函數(shù)名,子測(cè)試函數(shù)必須符合這種類型:func(*testing.T)。

清單 12

// TestAdd tests the Add function.
func TestAdd(t *testing.T) {
    tt := []struct {
        Name     string
        Operands []int
        Sum      int
    }{
        {
            Name:     "NoOperands",
            Operands: []int{},
            Sum:      0,
        },
        {
            Name:     "OneOperand",
            Operands: []int{10},
            Sum:      10,
        },
        {
            Name:     "TwoOperands",
            Operands: []int{10, 5},
            Sum:      15,
        },
        {
            Name:     "ThreeOperands",
            Operands: []int{10, 5, 4},
            Sum:      19,
        },
    }

    for _, test := range tt {
        fn := func(t *testing.T) {
            if e, a := test.Sum, Add(test.Operands...); e != a {
                t.Errorf("expected sum %d, got sum %d", e, a)
            }
        }

        t.Run(test.Name, fn)
    }
}

測(cè)試清單 12 中,使用匿名聲明的結(jié)構(gòu)體定義了不同的情況。遍歷這些情況,執(zhí)行這些測(cè)試用例。比較實(shí)際返回值和預(yù)期值,如果不等,則調(diào)用 t.Errorf,返回測(cè)試失敗的信息。清單中,遍歷調(diào)用 t.Run 執(zhí)行每個(gè)測(cè)試用例。

t.Helper() 和 t.Parallel()

標(biāo)準(zhǔn)庫中的 testing 包提供了很多有用的程序(函數(shù))輔助測(cè)試,而不用導(dǎo)入之外的第三方包。其中我最喜歡的兩個(gè)函數(shù)是 t.Helper() 和 t.Parallel(),它們都定義為 testing.T 接收者,它是在 _test.go 文件中每個(gè) Test 函數(shù)都必需的一個(gè)的參數(shù)。

清單 13

// GenerateTempFile generates a temp file and returns the reference to
// the underlying os.File and an error.
func GenerateTempFile() (*os.File, error) {
    f, err := ioutil.TempFile("", "")
    if err != nil {
        return nil, err
    }

    return f, nil
}

在代碼清單 13 中,為特定的測(cè)試包定義了一個(gè)輔助函數(shù)。這個(gè)函數(shù)返回 os.File 指針和error。每次測(cè)試調(diào)用這個(gè)輔助函數(shù)必須判斷 error 是一個(gè) non-nil 。通常情況這也沒什么,但是有一個(gè)更好的方式:使用 t.Helper() ,這種方式省略了 error 返回。

清單 14

// GenerateTempFile generates a temp file and returns the reference to
// the underlying os.File.
func GenerateTempFile(t *testing.T) *os.File {
    t.Helper()

    f, err := ioutil.TempFile("", "")
    if err != nil {
        t.Fatalf("unable to generate temp file: %v", err)
    }

    return f
}

清單 14 和清單 13 相同,只是使用 t.Helper()。這個(gè)函數(shù)定義使用了 *testing.T 作為參數(shù),省略了 error 的返回。函數(shù)先調(diào)用 t.Helper(),這在編譯測(cè)試二進(jìn)制文件時(shí)發(fā)出信號(hào):如果 t 在這個(gè)函數(shù)中調(diào)用任何接收器函數(shù),則將其報(bào)告給調(diào)用函數(shù)(Test*)。與輔助函數(shù)不同,所有行號(hào)和文件信息會(huì)都會(huì)關(guān)聯(lián)到這個(gè)函數(shù)。

一些測(cè)試可以進(jìn)行安全的并行進(jìn)行,并且 Go testing 包原生支持并行運(yùn)行測(cè)試。在所有 Test* 函數(shù)開始調(diào)用 t.Parallel(), 可以編譯出可以安全并行運(yùn)行的測(cè)試二進(jìn)制文件。就是這么簡單,就是這么強(qiáng)大!

結(jié)論

如果不配置程序運(yùn)行時(shí)所需的外部系統(tǒng),則無法在集成測(cè)試的上下文中完全驗(yàn)證程序的行為。此外,需要持續(xù)監(jiān)測(cè)那些外部系統(tǒng)(特別是當(dāng)它們包含應(yīng)用程序狀態(tài)數(shù)據(jù)的情況下),以確保它們包含有效和有意義的數(shù)據(jù)。Go 使開發(fā)人員不僅可以在測(cè)試過程中進(jìn)行配置,還可以無需標(biāo)準(zhǔn)庫之外的包就能維護(hù)外部數(shù)據(jù)。因此,我們可以編寫可讀性,一致性,性能和可靠性同時(shí)都能保證的集成測(cè)試。Go 的真正魅力正在于其簡約而功能齊全的工具集,它為開發(fā)人員提供了無需依賴外部庫或任何非常規(guī)限制的功能。


via: https://www.ardanlabs.com/blog/2019/10/integration-testing-in-go-set-up-and-writing-tests.html

作者:George Shaw[5]譯者:TomatoAres[6]校對(duì):lxbwolf[7]

本文由 GCTT[8] 原創(chuàng)編譯,Go 中文網(wǎng)[9] 榮譽(yù)推出

 

分享到:
標(biāo)簽:代碼
用戶無頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績?cè)u(píng)定