httptestでmock server的なものを作る方法のメモ

以下の3つが欲しい

  • get
  • post (form)
  • post (json)
package m

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "net/url"
    "testing"

    "io/ioutil"

    "github.com/pkg/errors"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestGet(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Exactly(t, "/foo", r.URL.Path)
        assert.Exactly(t, "1", r.URL.Query().Get("value"))
    }))
    defer ts.Close()

    _, err := http.Get(ts.URL + "/foo?value=1")
    require.NoError(t, err)
}

func TestPost(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Exactly(t, "/foo", r.URL.Path)
        r.ParseForm()
        assert.Exactly(t, "1", r.Form.Get("value"))
    }))
    defer ts.Close()
    values := url.Values{}
    values.Add("value", "1")
    _, err := http.PostForm(ts.URL+"/foo", values)
    require.NoError(t, err)
}

func TestPostJSON(t *testing.T) {
    type data struct {
        Name string `json:"name"`
        Age  int    `json:"int"`
    }

    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        assert.Exactly(t, "/foo", r.URL.Path)
        var val data
        parseJSONRequest(r, func(b []byte) error {
            return json.Unmarshal(b, &val)
        })
        assert.Exactly(t, "foo", val.Name)
        assert.Exactly(t, 20, val.Age)
    }))
    defer ts.Close()

    dataset := data{
        Name: "foo",
        Age:  20,
    }

    b, err := json.Marshal(dataset)
    require.NoError(t, err)
    req, err := http.NewRequest("POST", ts.URL+"/foo", bytes.NewBuffer(b))
    req.Header.Set("Content-Type", "application/json")

    _, err = (&http.Client{}).Do(req)
    require.NoError(t, err)
}

func parseJSONRequest(r *http.Request, parse func(body []byte) error) error {
    if r.Body == nil {
        return errors.New("missing form body")
    }
    ct := r.Header.Get("Content-Type")
    if ct != "application/json" {
        return errors.Errorf("invalid content type: %v", ct)
    }
    b, err := ioutil.ReadAll(r.Body)
    if err != nil {
        return err
    }
    return parse(b)
}

magicalimportというライブラリを作ってました

magicalimportというライブラリを作ってました。

はじめに

これは何をするライブラリかというと物理的なファイル名を指定して、指定したファイルをpython moduleとしてimportするためのライブラリです。

用途

例えばconfigファイルの読み込みに便利かもしれません。

使い方

例えば、以下のようなファイル構造の時に、以下のようなfoo.pyがあった時に。

.
├── a
│   └── b
│       └── c
│           └── foo.py
└── main.py

a/b/c/foo.py

name = "foo"
_age = "*secret*"

main.pyでは以下の様なコードでfoo.pyを読み込むことができます。

from magicalimport import import_from_physical_path

foo = import_from_physical_path("./a/b/c/foo.py")

ちなみに、importするmodule名を指定する事もできて、 as_ オプションを付けます。sys.modulesに登録されるのでその後は普通にimportできます。

from magicalimport import import_from_physical_path

foo = import_from_physical_path("./a/b/c/foo.py", as_="foo2")
import foo2
# fooとfoo2は同じもの

注意点

moduleの階層構造に関係なくimportしているところがあるので読み込んだ先のファイルでrelative importなどは上手くいかないです。

例えば、以下のような設定ファイルの構造でlocal.py,test.pyがbase.pyの設定を共有したいときなどに。

config
├── base.py
├── test.py
└── local.py

普通にrelative importしたくなりますがこれは動きません。

from .base import *

以下の様に書く必要があります。star importする場合にはexpose_all_membersを使うと便利です。

# 以下はだいたい `from .base import *` と同じ
import magicalimport
import os.path


here = os.path.dirname(os.path.abspath(__file__))
base = magicalimport.import_from_physical_path(os.path.join(here, "./base.py"), as_="base")
magicalimport.expose_all_members(base)

moduleの"_"で始まるものはimportされないですが。

追記

python2もサポートしました

追記

here オプションをサポートしました。以下の様に書ける様になりました。

import magicalimport


base = magicalimport.import_from_physical_path("base.py", as_="base", here=__file__)
magicalimport.expose_all_members(base)