您确实可以开发安全的GraphQL API吗? FastAPI GraphQL测试,用于安全的API开发


介绍

本文是ZOZO Technologies Advent Calendar#2的第16天!

TL; DR

  • 解释在REST上使用GraphQL的好处
  • 使用ORM操纵Postgres
  • 描述在GraphQL中使用的模式,变异和查询
  • 使用Graphene使GraphQL在Fast API上可用
  • 使用Graphene和pytest测试GraphQL API

为什么在REST上使用GraphQL

REST是用于构建Web API的事实上的标准。为每个CRUD操作设计多个端点(GET,POST,PUT,DELETE)。您可以通过访问这些端点来收集所需的信息。

例如,如果要获取特定的用户信息以及帖子和相关评论,则需要调用四个不同的端点:

  • /users/<id>返回初始用户数据

  • /users/<id>/posts返回特定用户的所有帖子

  • /users/<post_id>/comments返回每个帖子的评论列表

  • /users/<id>/comments返回每个用户的评论列表

  • 每个端点都很简单,但是在获得所需信息之前,您获得的数据超出了您的需要。

    在RESTful API中,为了获取没有过多或不足的数据,请请求过度提取(获取包括未使用的数据)和欠获取(由于数据不足而获取下一个端点)是很常见的。

    另一方面,GraphQL是用于从API检索数据的查询语言。它没有多个端点,而是围绕单个端点构建的,该端点取决于客户端的需求。

    GraphQL配置如下查询,以获取用户信息,帖子和评论。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    query {
      User(userId: 2){
        name
        posts {
          title
          comments {
            body
          }
        }
        comments {
          body
        }
      }
    }

    这使您可以在一个请求中获取所需的所有数据,而不会超量获取。

    制备

    创建一个名为

    「fastapi-graphql」的项目文件夹。

    1
    2
    $ mkdir fastapi-graphql
    $ cd fastapi-graphql

    接下来,创建并激活一个新的Python虚拟环境。

    1
    2
    $ python3.9 -m venv env
    $ source env/bin/activate

    创建requirements.txt以安装依赖项。

    requirements.txt

    1
    2
    fastapi==0.61.1
    uvicorn==0.12.2

    根据创建的依赖文件安装软件包。

    1
    $ pip install -r requirements.txt

    接下来,创建main.py,它描述了一个简单的API流程来检查操作。

    main.py

    1
    2
    3
    4
    5
    6
    7
    8
    from fastapi import FastAPI

    app = FastAPI()


    @app.get('/')
    def ping():
        return {'ping': 'pong'}

    启动应用程序

    1
    $ uvicorn main:app --reload

    如果运行正常,则应该可以访问以下URL。
    http://本地主机:8080 / ping

    如果输出这样的结果,则可以。

    1
    2
    3
    {
        "ping": "pong"
    }

    另外,在FastAPI中,会自动生成Swagger的API文档。
    HTTP:// //本地主机:8000 / docs
    swagger-docs.png

    Postgres

    接下来,下载,安装并启动Postgres。

    设定

    进行设置以使用PostgreSQL。
    *在下文中,以Ubuntu为例。对于其他环境,请根据适当的方法进行设置。

    切换到使用Postgres

    的帐户

    1
    sudo -u postgres -i

    用户和密码设置

    1
    createuser -d -U postgres -P db_user

    用户名是db_user
    接下来,将要求您输入密码,因此输入db_password

    创建数据库

    1
    createdb db_name --encoding=UTF-8 --owner=db_user

    创建为

    数据库名称db_name

    连接测试

    执行以下命令后,如果可以确认可以正确输入密码db_password并进入交互模式,则设置完成。

    1
    2
    3
    4
    5
    6
    7
    $ psql -U db_user -h localhost -d db_name
    Password for user db_user:
    psql (13.1 (Ubuntu 13.1-1.pgdg20.04+1))
    SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
    Type "help" for help.

    db_name=>

    您可以使用

    exit命令退出该模式。

    Python侧面设置

    将相关的依赖程序包添加到requirements.txt文件中,以使用Fast API。

    requirements.txt

    1
    2
    3
    4
    fastapi==0.61.1
    uvicorn==0.12.2
    orator==0.9.9 # 追加
    psycopg2-binary==2.8.6 # 追加

    安装

    1
    $ pip install -r requirements.txt

    创建一个db.py文件以连接到数据库。

    db.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    from orator import DatabaseManager, Schema, Model

    DATABASES = {
        "postgres": {
            "driver": "postgres",
            "host": "localhost",
            "database": "db_name",
            "user": "db_user",
            "password": "db_password",
            "prefix": "",
            "port": 5432
        }
    }

    db = DatabaseManager(DATABASES)
    schema = Schema(db)
    Model.set_connection_resolver(db)

    请根据需要更改以下数据库信息。

    • 数据库名称:db_name

    • 用户名:db_user

    • 密码:db_password

    建立模型

    接下来,为用户,帖子和评论创建一个模型。

    用户模型

    使用

    orator命令创建一个User模型。

    1
    $ orator make:model User -m

    -m是用于创建迁移文件的选项。
    *在此阶段,它尚未应用于数据库。

    执行

    命令后,如果显示以下消息,则表示成功。

    1
    2
    Model User successfully created.
    Created migration: 2020_12_14_150844_create_users_table.py

    另外,将在日历目录中创建migrationsmodels文件夹。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .
    ├── db.py
    ├── main.py
    ├── migrations
    │   ├── 2020_12_14_150844_create_users_table.py
    │   └── __init__.py
    ├── models
    │   ├── __init__.py
    │   └── user.py
    ├── requirements.txt
    └── setting.sh

    向用户模型

    添加了属性

    将以下各项添加到创建的迁移文件中以添加用户信息。

    • 名称
    • 街道地址
    • 电话号码
    • 性别

    添加

    的位置在table.increments('id')之后。

    ./migrations/2020_12_14_150844_create_users_table.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    from orator.migrations import Migration


    class CreateUsersTable(Migration):

        def up(self):
            """
            Run the migrations.
            """
            with self.schema.create('users') as table:
                table.increments('id')
                table.string('name') # 追加
                table.text('address') # 追加
                table.string('phone_number', 11) # 追加
                table.enum('sex', ['male', 'female']) # 追加
                table.timestamps()

        def down(self):
            """
            Revert the migrations.
            """
            self.schema.drop('users')

    邮编

    接下来,创建一个Post模型。

    1
    $ orator make:model Post -m

    向邮政模型

    添加了属性

    Post模型的必需属性添加到创建的迁移文件中。

    ./migrations/2020_12_14_153522_create_posts_table.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    from orator.migrations import Migration


    class CreatePostsTable(Migration):

        def up(self):
            """
            Run the migrations.
            """
            with self.schema.create('posts') as table:
                table.increments('id')
                table.integer('user_id').unsigned() # 追加
                table.foreign('user_id').references('id').on('users') # 追加
                table.string('title') # 追加
                table.text('body') # 追加
                table.timestamps()

        def down(self):
            """
            Revert the migrations.
            """
            self.schema.drop('posts')

    在这里,下面的列代表外键。
    它引用users表中的id

    1
    2
    table.integer('user_id').unsigned()
    table.foreign('user_id').references('id').on('users')

    评论模型

    最后,创建一个注释模型。

    1
    $ orator make:model Comments -m

    向模型

    添加属性

    将属性添加到创建的迁移文件中。

    ./migrations/2020_12_14_153529_create_comments_table.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    from orator.migrations import Migration


    class CreateCommentsTable(Migration):

        def up(self):
            """
            Run the migrations.
            """
            with self.schema.create('comments') as table:
                table.increments('id')
                table.integer('user_id').unsigned().nullable() # 追加
                table.foreign('user_id').references('id').on('users') # 追加
                table.integer('post_id').unsigned().nullable() # 追加
                table.foreign('post_id').references('id').on('posts') # 追加
                table.text('body') # 追加
                table.timestamps()

        def down(self):
            """
            Revert the migrations.
            """
            self.schema.drop('comments')

    执行迁移

    使用以下命令执行迁移。

    1
    $ orator migrate -c db.py

    yes/no将询问您,因此请回答yes
    这将在数??据库中创建userspostscomments表。

    1
    2
    3
    4
    5
    6
    Are you sure you want to proceed with the migration?  (yes/no) [no] yes

    Migration table created successfully
    [OK] Migrated 2020_12_14_150844_create_users_table
    [OK] Migrated 2020_12_14_153522_create_posts_table
    [OK] Migrated 2020_12_14_153529_create_comments_table

    关系设置

    接下来,编辑在./models下生成的文件,以设置User模型与Postcomments模型之间的关系。

    用户模型

    您可以通过应用

    has_many装饰器来建立一对多关系。
    下面,我们显示User模型,Post模型和comments模型具有一对多关系。

    ./models/user.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    from orator.orm import has_many

    from db import Model


    class User(Model):

        @has_many
        def posts(self):
            from .post import Post

            return Post

        @has_many
        def comments(self):
            from .comment import Comments

            return Comments

    邮编

    comments设置与模型的关系。

    ./models/post.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    from orator.orm import has_many

    from db import Model


    class Post(Model):

        @has_many
        def comments(self):
            from .comment import Comments

            return Comments

    评论模型

    comments在模型中未设置任何内容。

    ./models/comment.py

    1
    2
    3
    4
    5
    6
    from orator import Model


    class Comments(Model):

        pass

    GraphQL

    要在Fast API上构建GraphQL API,您需要安装Graphene。

    石墨烯安装

    让我们将Graphene包添加到

    require.txt。

    1
    2
    3
    4
    5
    fastapi==0.61.1
    uvicorn==0.12.2
    orator==0.9.9
    psycopg2-binary==2.8.6
    graphene==2.1.8 # 追加

    安装。

    1
    $ pip install -r requirements.txt

    什么是架构

    GraphQL模式是GraphQL API规范的表示。
    该模式汇总所有关系和类型定义。

    创建架构

    在项目根目录中创建schema.py

    ./schema.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import graphene


    class Query(graphene.ObjectType):
        say_hello = graphene.String(name=graphene.String(default_value='Test Driven'))

        @staticmethod
        def resolve_say_hello(parent, info, name):
            return f'Hello {name}'

    与FastAPI

    合作

    更新main.py以加载从快速API创建的架构。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import graphene
    from fastapi import FastAPI
    from starlette.graphql import GraphQLApp

    from schema import Query

    app = FastAPI()

    app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=Query)))

    @app.get('/')
    def ping():
        return {'ping': 'pong'}

    在这里,我们将模式传递给starletter的GraphQLApp以处理GraphQl。

    1
    app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=Query)))

    重新引导服务器。

    1
    $ uvicorn main:app --reload

    GraphiQL

    通过访问以下内容,可以使用可以交互执行GraphQL查询的GraphQL。
    http://本地主机:8000 / graphql

    GraphQL查询执行

    在左侧窗口中粘贴以下查询并执行。

    1
    2
    3
    query {
      sayHello(name: "Taro")
    }

    确保返回以下内容。

    1
    2
    3
    4
    5
    {
      "data": {
        "sayHello": "Hello Taro"
      }
    }

    image.png

    GraphQL Pydantic

    在这里,我们将创建一个Pydantic模型,以验证GraphQL API中的类型提示和查询。

    为此,将石墨烯-pydantic软件包添加到requirements.txt

    requirements.txt

    1
    2
    3
    4
    5
    6
    fastapi==0.61.1
    uvicorn==0.12.2
    orator==0.9.9
    psycopg2-binary==2.8.6
    graphene==2.1.8
    graphene-pydantic==0.1.0 # 追加

    安装

    1
    $ pip install -r requirements.txt

    项目根目录中创建一个名为serializers.py的文件,以为输入和输出对象创建一个pydantic模型。

    ./serializers.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    from typing import List, Optional

    from graphene_pydantic import PydanticInputObjectType, PydanticObjectType
    from pydantic import BaseModel


    class CommentsModel(BaseModel):
        id: int
        user_id: int
        post_id: int
        body: str


    class PostModel(BaseModel):
        id: int
        user_id: int
        title: str
        body: str
        comments: Optional[List[CommentsModel]]


    class UserModel(BaseModel):
        id: int
        name: str
        address: str
        phone_number: str
        sex: str
        posts: Optional[List[PostModel]]
        comments: Optional[List[CommentsModel]]


    class CommentGrapheneModel(PydanticObjectType):
        class Meta:
            model = CommentsModel


    class PostGrapheneModel(PydanticObjectType):
        class Meta:
            model = PostModel


    class UserGrapheneModel(PydanticObjectType):
        class Meta:
            model = UserModel


    class CommentGrapheneInputModel(PydanticInputObjectType):
        class Meta:
            model = CommentsModel
            exclude_fields = ('id', )


    class PostGrapheneInputModel(PydanticInputObjectType):
        class Meta:
            model = PostModel
            exclude_fields = ('id', 'comments')


    class UserGrapheneInputModel(PydanticInputObjectType):
        class Meta:
            model = UserModel
            exclude_fields = ('id', 'posts', 'comments')

    导入的PydanticInputObjectType类和PydanticObjectType类分别与输入和输出的pydantic模型相关联,并与User模型,Post模型和comments模型相关联。

    此处,在Meta exclude_fields中,每个模型中自动生成的ID被排除在验证之外。

    突变

    GraphQL使用突变来修改数据。它主要在创建,更新和删除数据时使用。

    创建创建对象

    接下来,让我们使用突变来创建UserPostComment对象,并将其保存在数据库中。

    用以下代码更新schema.py文件。

    ./schema.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    import graphene

    from serializers import (
        UserGrapheneInputModel,
        UserGrapheneModel,
        PostGrapheneInputModel,
        PostGrapheneModel,
        CommentGrapheneInputModel,
        CommentGrapheneModel,
    )

    from models.comment import Comments
    from models.post import Post
    from models.user import User


    class Query(graphene.ObjectType):
        say_hello = graphene.String(name=graphene.String(default_value='Test Driven'))

        @staticmethod
        def resolve_say_hello(parent, info, name):
            return f'Hello {name}'


    class CreateUser(graphene.Mutation):
        class Arguments:
            user_details = UserGrapheneInputModel()

        Output = UserGrapheneModel

        @staticmethod
        def mutate(parent, info, user_details):
            user = User()
            user.name = user_details.name
            user.address = user_details.address
            user.phone_number = user_details.phone_number
            user.sex = user_details.sex

            user.save()

            return user


    class CreatePost(graphene.Mutation):
        class Arguments:
            post_details = PostGrapheneInputModel()

        Output = PostGrapheneModel

        @staticmethod
        def mutate(parent, info, post_details):
            user = User.find_or_fail(post_details.user_id)
            post = Post()
            post.title = post_details.title
            post.body = post_details.body

            user.posts().save(post)

            return post


    class CreateComment(graphene.Mutation):
        class Arguments:
            comment_details = CommentGrapheneInputModel()

        Output = CommentGrapheneModel

        @staticmethod
        def mutate(parent, info, comment_details):
            user = User.find_or_fail(comment_details.user_id)
            post = Post.find_or_fail(comment_details.post_id)

            comment = Comments()
            comment.body = comment_details.body

            user.comments().save(comment)
            post.comments().save(comment)

            return comment


    class Mutation(graphene.ObjectType):
        create_user = CreateUser.Field()
        create_post = CreatePost.Field()
        create_comment = CreateComment.Field()

    我为每个类(CreateUserCreatePostCreateComment)定义了一个mutate方法,该方法将在调用

    突变时应用。

    突变

    的应用

    还要更新main.py文件以处理添加的突变。

    ./main.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import graphene
    from fastapi import FastAPI
    from starlette.graphql import GraphQLApp

    from schema import Query, Mutation

    app = FastAPI()

    app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=Query, mutation=Mutation)))

    @app.get('/')
    def ping():
        return {'ping': 'pong'}

    运行createUser突变

    重新启动Uvicorn,重新加载http://本地主机:8000 / graphql,在下面粘贴查询并运行createUser的突变。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    mutation createUser {
      createUser(userDetails: {
        name: "Max Kun",
        address: "Tokyo",
        phoneNumber: "12345678",
        sex: "male"
      })
      {
        id
        name
        posts {
          body
          comments {
            body
          }
        }
      }
    }

    如果收到以下响应,则说明您成功。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
      "data": {
        "createUser": {
          "id": 2,
          "name": "Max Kun",
          "posts": []
        }
      }
    }

    image.png

    运行createPost突变

    接下来,让我们执行CreatePost突变并发表新文章。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mutation createPost {
      createPost(postDetails: {
        userId: 2,
        title: "Hello",
        body: "This is the first post."
      })
      {
        id
      }
    }

    您将收到以下响应。

    1
    2
    3
    4
    5
    6
    7
    {
      "data": {
        "createPost": {
          "id": 1
        }
      }
    }

    createComments运行变异

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mutation createComment {
      createComment(commentDetails: {
        userId: 2,
        postId: 1,
        body: "This is a test comment."
      })
      {
        id
        body
      }
    }

    将返回以下响应。

    1
    2
    3
    4
    5
    6
    7
    8
    {
      "data": {
        "createComment": {
          "id": 1,
          "body": "This is a test comment."
        }
      }
    }

    询问

    GraphQL使用查询以列表或单个对象的形式检索数据。

    创建查询

    更新schema.pyQuery类以将用户列表作为列表获取。

    ./schema.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Query(graphene.ObjectType):
        say_hello = graphene.String(name=graphene.String(default_value='Test Driven'))
        list_users = graphene.List(UserGrapheneModel)

        @staticmethod
        def resolve_say_hello(parent, info, name):
            return f'Hello {name}'

        @staticmethod
        def resolve_list_users(parent, info):
            return User.all()

    获取用户列表

    重新加载GraphiQL并运行以下查询以获取用户列表。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    query getAllUsers {
      listUsers {
        id
        name
        posts {
          title
        }
      }
    }

    结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
      "data": {
        "listUsers": [
          {
            "id": 1,
            "name": "John Doe",
            "posts": []
          },
          {
            "id": 2,
            "name": "Max Kun",
            "posts": [
              {
                "title": "Hello"
              }
            ]
          }
        ]
      }
    }

    image.png

    特定用户信息的获取

    接下来,创建一个返回特定用户信息的查询。

    如示例中一样,更新schema.pyQuery类。

    ./schema.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Query(graphene.ObjectType):
        say_hello = graphene.String(name=graphene.String(default_value='Test Driven'))
        list_users = graphene.List(UserGrapheneModel)
        get_single_user = graphene.Field(UserGrapheneModel, user_id=graphene.NonNull(graphene.Int))

        @staticmethod
        def resolve_say_hello(parent, info, name):
            return f'Hello {name}'

        @staticmethod
        def resolve_list_users(parent, info):
            return User.all()

        @staticmethod
        def resolve_get_single_user(parent, info, user_id):
            return User.find_or_fail(user_id)

    演说者具有内置的find_or_fail函数,如果传递了无效的ID,则会引发异常。

    成功执行查询

    让我们通过指定正确的用户ID来获取用户信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    query getUser {
      getSingleUser(userId: 2) {
        name
        posts {
          title
          comments {
            body
          }
        }
        comments {
          body
        }
      }
    }

    结果:我得到了特定用户信息的列表以及预期的发布和评论。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    {
      "data": {
        "getSingleUser": {
          "name": "Max Kun",
          "posts": [
            {
              "title": "Hello",
              "comments": [
                {
                  "body": "This is a test comment."
                }
              ]
            }
          ],
          "comments": [
            {
              "body": "This is a test comment."
            }
          ]
        }
      }
    }

    image.png

    如果不需要帖子或评论信息,请按以下方式修改查询:

    1
    2
    3
    4
    5
    query getUser {
      getSingleUser(userId: 2) {
        name
      }
    }

    结果:我刚得到用户信息。

    1
    2
    3
    4
    5
    6
    7
    {
      "data": {
        "getSingleUser": {
          "name": "Max Kun"
        }
      }
    }

    image.png

    如果您指定了错误的用户ID

    如果我使用错误的用户ID运行查询该怎么办?

    1
    2
    3
    4
    5
    query getUser {
      getSingleUser(userId: 9999) {
        name
      }
    }

    结果:返回错误。
    如前所述,Orator具有内置的find_or_fail函数,如果传递了无效的ID,则会引发异常。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
      "data": {
        "getSingleUser": null
      },
      "errors": [
        {
          "message": "No query results found for model [User]",
          "locations": [
            {
              "line": 2,
              "column": 3
            }
          ],
          "path": [
            "getSingleUser"
          ]
        }
      ]
    }

    image.png

    测试

    Graphene提供了用于测试Graphene应用程序的测试客户端。

    准备

    由于我们使用的是

    pytest,因此需要向requirements.txt添加依赖项。

    requirements.txt

    1
    2
    3
    4
    5
    6
    7
    fastapi==0.61.1
    uvicorn==0.12.2
    orator==0.9.9
    psycopg2-binary==2.8.6
    graphene==2.1.8
    graphene-pydantic==0.1.0
    pytest==6.1.1 # 追加

    安装

    1
    $ pip install -r requirements.txt

    创建测试预处理

    接下来,在项目根目录中创建一个「test」文件夹,并在其下添加conftest.py文件。
    在这里,描述执行测试之前要执行的过程。

    ./test/conftest.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    import graphene
    import pytest
    from graphene.test import Client
    from orator import DatabaseManager, Model, Schema
    from orator.migrations import DatabaseMigrationRepository, Migrator

    from models.comment import Comments
    from models.post import Post
    from models.user import User
    from schema import Query, Mutation


    @pytest.fixture(autouse=True)
    def setup_database():
        DATABASES = {
            "sqlite": {
                "driver": "sqlite",
                "database": "test.db"
            }
        }

        db = DatabaseManager(DATABASES)
        Schema(db)

        Model.set_connection_resolver(db)

        repository = DatabaseMigrationRepository(db, "migrations")
        migrator = Migrator(repository, db)

        if not repository.repository_exists():
            repository.create_repository()

        migrator.reset("migrations")
        migrator.run("migrations")


    @pytest.fixture(scope="module")
    def client():
        client = Client(schema=graphene.Schema(query=Query, mutation=Mutation))
        return client


    @pytest.fixture(scope="function")
    def user():
        user = User()
        user.name = "John Doe"
        user.address = "United States of Nigeria"
        user.phone_number = 123456789
        user.sex = "male"
        user.save()

        return user


    @pytest.fixture(scope="function")
    def post(user):
        post = Post()
        post.title = "Test Title"
        post.body = "this is the post body and can be as long as possible"

        user.posts().save(post)
        return post


    @pytest.fixture(scope="function")
    def comment(user, post):
        comment = Comments()
        comment.body = "This is a comment body"

        user.comments().save(comment)
        post.comments().save(comment)

        return comment

    下面描述代码的详细信息。

    关于数据库固定装置

    此处定义了数据库设置和迁移应用程序测试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @pytest.fixture(autouse=True)
    def setup_database():
        DATABASES = {
            "sqlite": {
                "driver": "sqlite",
                "database": "test.db"
            }
        }

        db = DatabaseManager(DATABASES)
        Schema(db)

        Model.set_connection_resolver(db)

        repository = DatabaseMigrationRepository(db, "migrations")
        migrator = Migrator(repository, db)

        if not repository.repository_exists():
            repository.create_repository()

        migrator.reset("migrations")
        migrator.run("migrations")

    通过在

    @pytest.fixture装饰器的参数中将autouse指定为True,可以定义在执行测试功能之前要执行的功能。

    在执行每个测试功能之前,将调用

    setup_database函数,并初始化数据库。

    石墨烯客户端夹具

    石墨烯这是用于创建测试客户端的装置。

    1
    2
    3
    4
    @pytest.fixture(scope="module")
    def client():
        client = Client(schema=graphene.Schema(query=Query, mutation=Mutation))
        return client

    参数中指定的scope可以控制执行固定装置的粒度。

    <表格>

    范围名称

    执行粒度


    <身体>

    功能

    每个测试用例运行一次(默认)

    为整个测试类运行一次

    模块

    对整个测试文件运行一次

    会话

    在整个测试中只能运行一次


    其他灯具

    这是用于创建用户,帖子和评论信息的装置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    @pytest.fixture(scope="function")
    def user():
        user = User()
        user.name = "John Doe"
        user.address = "United States of Nigeria"
        user.phone_number = 123456789
        user.sex = "male"
        user.save()

        return user


    @pytest.fixture(scope="function")
    def post(user):
        post = Post()
        post.title = "Test Title"
        post.body = "this is the post body and can be as long as possible"

        user.posts().save(post)
        return post


    @pytest.fixture(scope="function")
    def comment(user, post):
        comment = Comments()
        comment.body = "This is a comment body"

        user.comments().save(comment)
        post.comments().save(comment)

        return comment

    创建测试

    创建一个名为

    test_query.py的测试文件,并为您的用户模型添加一个测试,如下所示:

    ./test/test_query.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    def test_create_user(client):
        query = """
        mutation {
            createUser(userDetails: {
                name: "Test User",
                sex: "male",
                address: "My Address",
                phoneNumber: "123456789",
            })
            {
                id
                name
                address
            }
        }
        """

        result = client.execute(query)
        assert result['data']['createUser']['id'] == 1
        assert result['data']['createUser']['name'] == "Test User"


    def test_get_user_list(client, user):
        query = """
        query {
            listUsers {
                name
                address
            }
        }
        """

        result = client.execute(query)
        assert type(result['data']['listUsers']) == list


    def test_get_single_user(client, user):
        query = """
        query {
            getSingleUser(userId: %s){
                address
            }
        }
        """ % user.id
        result = client.execute(query)

        assert result['data']['getSingleUser'] is not None
        assert result['data']['getSingleUser']['address'] == user.address

    运行测试

    使用以下命令在pytest中运行测试:

    1
    $ python -m pytest -s test

    执行

    命令后,将执行所有测试项目。

    1
    2
    3
    4
    5
    6
    7
    ================================================ test session starts ================================================
    platform linux -- Python 3.9.1, pytest-6.1.1, py-1.10.0, pluggy-0.13.1
    rootdir: /home/taro/Workspace/Python/fastapi-graphql
    collected 3 items                                                                                                  

    test/test_query.py ...
    =========================================== 3 passed in 0.51s =======================================================

    测试成功通过????

    概要

    介绍了如何使用FastAPI,Orator ORM和pytest开发和测试GraphQL API。

    快速API作为标准提供类型提示和验证,因此它似乎与使用Pydantic的GraphQL API开发兼容。

    明天是ZOZO Technologies出现日历#2 @yokawasa的文章!期待!