欢迎来到赛兔子家园

unittest单元测试框架&接口介绍使用

本篇内容:

  • unittest单元测试框架
  • 接口相关概念

一、Django中进行单元测试

1、unittest framework

  unittest单元测试框架,单元测试框架不单单适用于程序单元级别的测试。

      unittest官网帮助文档:https://docs.python.org/zh-cn/3/library/unittest.html

  单元测试框架主要完成以下事项:

     a) 提供用例组织和执行

            当测试用例只有几条时,可以不必考虑用例的组织,但是当测试用例达到成百上千条时,大量测试用例堆在一起是就产生了扩展性与维护性等问题,此时就需要考虑用例的规范与组织问题。

     b) 提供丰富的比较方法

        不论功能测试,还是单元测试,在用例执行完成之后都需要将实际结果与预期结果进行比较(断言),从而断定用例是否执行通过。此时,单元测试框架提供了丰富的断言方法。

           例如:判断相等/不等、包含/不包含、True/False的断言方法等。

     c)  提供丰富的日志

            当测试用例执行失败时能抛出清晰的失败原因,当所有用例执行完成后能提供丰富执行情况结果信息。

            例如:总执行时间、失败用例数、成功用例数等。

       从以上几点来看,单元测试框架可以帮助我们完成任何级别的自动化测试。

  •       单元测试  unittest
  •       HTTP接口自动化测试   : unittest  + Requests
  •       Web UI自动化测试     :  unittest  + Selenium
  •       移动自动化测试          :  unittest  + Appium

例如:开发一个简单的计算器,用于两个数的加、减、乘、除功能

count.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian

class  Calculator:
    ''' 实现两个数加、减、乘、除
    '''
    def __init__(self,a,b):
        self.a = int(a)
        self.b = int(b)

    #加法

    def add(self):

        return self.a + self.b

    #减法

    def sub(self):
        return self.a - self.b

    #乘法
    def mul(self):
        return self.a * self.b

    #除法
def div(self): return self.a / self.b

通过unittest单元测试框架编写测试用例

test_count.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian

import unittest

from  count import Calculator

class CountTest(unittest.TestCase):  #继承unittest.TestCase类

    def setUp(self):

        self.cal  = Calculator(8,8) #测试用例开始时执行

    def tearDown(self): #测试用例结束时执行
        pass

    def test_add(self):
        result = self.cal.add()
        self.assertEqual(result,16)

    def test_sub(self):
        result = self.cal.sub()
        self.assertEqual(result,0)


    def test_mul(self):
          result = self.cal.mul()
          self.assertEqual(result,64)

    def test_div(self):
          result = self.cal.div()
          self.assertEqual(result,1)

if __name__ == '__main__':

    # unittest.main()  执行全部测试用例

    #构造测试集,测试用例集要通过cmd下执行,才会生效
    suite = unittest.TestSuite()
    suite.addTest(CountTest("test_add"))
    suite.addTest(CountTest("test_sub"))
    suite.addTest(CountTest("test_mul"))
   # suite.addTest(CountTest("test_div"))

    #执行测试

    runner = unittest.TextTestRunner()
    runner.run(suite)

  构造测试集,需要在cmd下面执行才会生效

  通过unittest单元测试框架编写测试用例更加规范和整洁。接下来,分析一下unittest单元测试用例:

     首先,通过import导入unittest单元测试框架,创建CoutTest类继承unittest.TestCase类。

  setUp()和tearDown()在单元测试框架中比较特别,分别在每个测试用例的开始和结束执行:

  •   setUp()方法用于测试用例执行前的初始化工作,例如【测试化变量、生成数据库测试数据、打开浏览器、创建对象等】;
  •   tearDown()方法与setUp()方法相呼应,用于测试用例执行之后的善后工作,例如【清除数据库测试数据、关闭文件、关闭浏览器】;

  unittest要求测试方法必须以"test"开头。例如:test_add/test_sub等;

  调用unittest.TextTestRunner()类中的run()方法运行测试套件中的测试用例,如果想默认运行当前测试文件下的所有测试用例,使用unittest.main()方法。main()方法在查找用例的时候根据两个规则:

  •  测试类必须继承unittest.TestCase类  ;
  •  该测试类下面的方法必须以test开头   ;

执行结果如下:

cmd.exe

>>python test_count.py

 

PS:执行结果通过一个小点"."来表示一条运行通过的用例,总共运行3条测试用例,用时0.001秒

2、Django的单元测试使用Python标准库模块

         unittest该模块定义使用基于类的方法测试,在我们创建Django应用时,默认已经帮我们生成了tests.py文件;

打开tests.py 编写测试代码:

from django.test import TestCase

# Create your tests here.

from django.test import TestCase
from sign.models import Event,Guest

class ModelTest(TestCase):

    def setUp(self):

        Event.objects.create(id=1,name="oneplus 3 event",status=True,limit=2000,
                             address='sengzhang',start_time="2017-05-27 16:12:25")

        Guest.objects.create(id=1,event_id=1,realname='alen',
                             phone='13642792566',email='alen1@mail.com',sign=False)

    def test_event_models(self):
        result = Event.objects.get(name="oneplus 3 event")

        self.assertEqual(result.address,"sengzhang")
        self.assertTrue(result.status)

    def test_guest_models(self):
        result = Guest.objects.get(phone='13642792566')
        self.assertEqual(result.realname,"alen")
        self.assertFalse(result.sign)

 首先 :创建ModelTest类,继承django.test的TestCase测试类;

 然后 :在setUp()初始化方法中,创建一条发布会和嘉宾数据   ;

 最后 :通过test_event_models()和test_guest_models()测试方法,分别查询两张表的数据,断言表中的数据是否正确。

 执行测试用例,切换到项目的根目录下,通过manage.py所提供的test命令执行测试

PS:Django在执行setUp()方法中的数据库初始化时,并非真正的向数据库表中插入了数据。所以,数据库并不会因为运行测试而产生测试数据;

      当编写完测试,最简单的方式是通过manage.py中“test"命令来直接执行所有测试。但是编写的测试用例越来越多的时候,测试运行的情况就复杂起来,比如要指定特定的测试模块,或测试类,又或者想执行测试文件名包含了"test”的文件。

通过参数可以控制Django项目下不同级别的测试:

运行sign应用下的所有测试用例:

运行sign应用下的tests.py测试文件 :

运行sign应用tests.py测试文件下的ModelTest测试类:

 运行ModelTest测试类下面的test_event_models:

指定匹配运行的测试文件-->test*.py,匹配以"test"开头,以.py结尾的测试文件:

二、接口相关概念

1、单元测试与模块测试

a、单元测试(Unit testing)

  • 单元测试是应用程序的最小可测试部分;
  • 在面向过程编程中,单元也可以是整个模块,但常见的是单个函数或过程;
  • 在面向对象编程中,单元通常是整个接口,例如类,但可以是单独的方法;
  • 单元测试多数情况下是由程序员自己完成的;

b、模块测试(Module testing)

 大多时候,我们认为单元测试与模块测试是一样的,通过以下定义我们读到了几个模块测试的解释:

  • 、首先,模块测试与单元测试有细微的区别;
  • 、模块测试是针对具有明显的功能特征的代码进行测试;
  • 、并且,它认为单元测试可能只涉及测试一小部分的功能;
  • 、模块测试多数情况下由其它程序员或测试人员进行;

 通过对单元测试和模块测试的概念分析,我们可以认为同一个事物用不同的两个角度去解释。单元测试更强调的是程序的最小可测试单元,而且模块测试更强调所测试程序的功能完整性;

 模块接口测试:对于这个叫法并没有找到规范的概念,它更多的只是一个口头概念。其实它就是模块测试加上“接口”两个字,更强调了被测试的模块有规范的输入和输出。因为这是一个可测试的模块最显著的特征之一;

2、接口测试

2.1)接口简介

程序接口

  关于程序接口,也可以看作是程序模块接口,具体到程序中一般就是提供了输入输出的类、方法或函数。对于程序接口的测试,一般需要使用与开发程序接口相同的编程语言。通过传入不同的参数,来验证程序接口的功能。

协议接口

  关于协议接口,一般指系统通过不同的协议来提供的接口,例如HTTP/SOAP协议等。这种类型接口对底层代码做了封装,通过协议的方式对外提供调用。因为不涉及程序层面,所以不受编程语言的限制:我们可以通过其它编程语言和工具进行测试。

2.2)接口分类

接口大体分为以下三类:

  • 系统与系统之间的接口

系统与系统之间的接口,这里可以是公司内部不同系统之间的接口调用,也可以是不同公司之间系统接口的调用;

  • 下层服务对上层服务的接口

 

 应用层      :从认识上可以看成是系统所提供的UI层功能,对于Web系统来说,可以认为是浏览器页面上所提供的功能:登录、注册、查询、删除等。

 Service层 :可以理解为服务器所提供数据和逻辑的处理。

 DB层       :数据库主要用来存放数据,例如用户的个人信息、商品的信息等。

访问对象  :它是一个面向对象的数据库访问接口。

  举例来说各层的工作过程:首先Service提供了一个查询接口--->这个接口需要一个参数(查询的关键字)--->然后应用层提供了一个输入框,需要用户输入查询关键字,并且还提供了一个查询按钮用于提交查询的关键字。当用户输入查询关键字并点击提交按钮后,相当于调用的查询接口,查询接口需要对用户提交的关键字做出相应的判断,是否为空?然后通过DAO层调用数据库,根据关键字查询表中的数据,最后再将拿到的数据返回给应用层,应用层负责将数据展示到Web页面上。

     在这个过程中,各层之间的交互就是通过接口,应用层与Service主要通过HTTTP接口。Service层与DB层主要通过DAO(Data Access Object)数据库访问接口:

  • 系统内,服务与服务之间的调用

  系统内部,服务与服务之间的调用,大多情况下是程序之间的调用;

  本质就是:程序开发的函数或类方法,提供入参与返回值;

 2.3)、接口测试的意义

    根据分层自动化测试中的定义,最底层有开发人员编写的单元禅道保证代码质量,最上层有功能测试人员(手工+UI自动化)进行大量的功能测试保证功能的可用。

     那么接口测试的意义是什么?

  • 更早的发现问题

   测试应该更早的介入到项目开发中,因为越早的发现bug,修复成本越低。然而功能测试必须要等到系统提供可测试的界面才能对系统进行测试。然而接口测试可以在功能界面开发出来之前对系统进行测试,系统接口是上层功能的基础,接口测试可以更早更低成本的发现和解决问题。

  • 缩短产品研发周期

     对于产品研发周期来说,如果将所有测试工作都集中在功能测试阶段,那么测试的问题和修复周期就会变长,因为测试可以更早的介入产品开发中,可以有效的控制功能阶段bug数量,从而有效的缩短产品开发周期;

  • 发现更底层的问题

          系统的有些底层逻辑是在UI功能测试中不太容易触发的,那么这些逻辑可能会存在问题。接口测试可以更容易更全面的测试到这些底层的逻辑。

  • 检查服务器的异常处理能力

    我们通常把前端的验证称为弱验证,因为它很容易被绕过,这个时候如果只站在功能的层面进行测试,很难发现一些安全的问题。接口测试就会很容易验证这些异常情况。

 三、开发Web接口

1、HTTP协议与JSON

  在正确开发Web接口之前先来简单介绍一下HTTP协议和Json数据格式。在当前Web接口开发中应用最普遍的就是HTTP协议,二JSON是目前非常流行的接口数据传输格式之一。

1.1)、HTTP协议

    超文本传输协议是互联网上应用最为广泛的一种网络协议。

          HTTP协议的主要特点概况如下:

  •      支持客户/服务器模式

     简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、POST。每种方法规定客户与服务器联系的类型不同。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度非常快。

  •     灵活    : HTTP允许传输任意类型的数据对象,正在传输的类型有Content-Type加以标记;
  •     无连接 : 无连接的含义是限制每次连接只处理一个请求。服务器处理完成客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  •     无状态 : HTTP协议是无状态的协议,无状态是指协议对应事务处理没有记忆能力,缺少状态意味着如果后续处理需要前面的信息,则它必须重传。这样可能导致每次连接传送的数据量增大,另一方面,在服务器不需要先前信息时它的应答就较快。

1.2)、HTTP请求类型

    请求行已用过方法符号开头,以空格分开,后面跟着请求的URL和协议的版本,格式如下:MethodRequest-URL HTTP-Version CRLF ,其中Method表示请求方法:Request-URL是一个统一资源标识符:HTTP-Version表示请求的HTTP协议版本:CRLF表示回车和换行(除了作为结尾的CRLF外,不允许出现单独的CR或LF字符)

 请求方法,各种方法的解释如下:

响应状态码:

状态代码有三位数字组成,第一个数字定义了响应的类别,且有五种可能取值:

1xx   :    表示请求已接收,继续处理 ;

2xx  :   成功--表示请求已被成功接收、理解、接受 ;

3xx   :   重定向--要完成请求必须进行更进一步的操作;

4xx   :  客户端错误--请求有语错误或请求无法实现    ;

5xx   :  服务器端错误--服务器未能实现合法的请求    ;

常见状态代码、状态说明:

200   OK                         // 客户端请求成功

400   Bad Request          //  客户端请求有语法错误,不能被服务器理解

401   Unauthorized        //   请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用

403   Forbidden            //    服务器收到的请求,但是拒绝提供服务

404   Not Found          // 请求资源不存在,输入错误的URL

500   Internal Server Error //服务器发生不可预期的错误

503   Server Unavailable   // 服务器当前不能出来客户端的请求,一段时间后可能恢复正常

1.3)、JSON

 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,易于人阅读和编写。

 

1.4)、什么是Web接口

接口只关心数据,它的输入和输出是具有一定格式的数据,接口并不关心数据展示在哪里,要以什么样式去展示。而HTML、CSS、JavaScript等关心的是数据展示在哪里,如何展示;

  一般Web接口返回数据如图:

 显然,这样的接口并不是直接给平台用户来使用的,它一般为其它开发者提供调用。后端与前端开发之间,不同系统的开发之间,以及不同公司的开发之间的调用。至于调用接口数据的开发者如何使用这些数据,对于接口开发者来说并不需要关心;

 

2、开发系统的Web接口

2.1)发布会添加接口

首先,单独创建/sign/views_if.py文件,开发添加发布会接口

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian
from django.http import JsonResponse
from sign.models import Event,Guest
import time
from django.db.utils import IntegrityError
from django.core.exceptions import  ValidationError
from django.core.exceptions import ValidationError,ObjectDoesNotExist


#添加发布会接口 ,测试ok
def add_event(request):

    eid        = request.POST.get('eid','')          #发布会id
    name       = request.POST.get('name','')         #发布会标题
    limit      = request.POST.get('limit','')        #限制人数
    status     = request.POST.get('status','')       #状态
    address    = request.POST.get('address','')      #地址
    start_time = request.POST.get('start_time','')   #发布会时间

    if eid =='' or name =='' or limit=='' or address=='' or start_time =='':

        return JsonResponse({'status':10021,'message':'parameter error'})

    result = Event.objects.filter(id=eid)

    if result: #新添加发布会id已经存在
        return JsonResponse({'status':10022,'message':'event id already exists'})


    result = Event.objects.filter(name = name)

    if result: #新添加发布会标题已经存在
        return JsonResponse({'status':10023,
                             'message':'event name already exists'})


    if status == '':
        status =1

    try:
        Event.objects.create(id=eid,name=name,limit=limit,address=address,status=int(status),
                             start_time=start_time)

    except ValidationError as e:

        error  = 'start_time format error .It must be in YYYY-MM-DD HH:MM:SS format.'
        return JsonResponse({'status':10024,'message':error})

    return JsonResponse({'status':200,'message':'add event success'})


#发布会查询接口 测试通过
def get_event_list(request):
    eid  = request.GET.get("eid","")   #发布会id
    name = request.GET.get("name","")  #发布会名称

    if eid == '' and name =='':
        return JsonResponse({'status':10021,'message':'parameter_error'})

    if eid !='':
        event = {}
        try:
            result = Event.objects.get(id=eid)  #get()方法只能精确查询

        except ObjectDoesNotExist:

            return JsonResponse({'status':10022,'message':'quer_result_is_empty'})

        else:
            event['name']       = result.name
            event['limit']      = result.limit
            event['status']     = result.status
            event['address']    = result.address
            event['start_time'] = result.start_time
            return JsonResponse({'status':200,'message':'success','data':event})

    if name != '':
        datas = []
        result = Event.objects.filter(name__contains=name) #模糊查询,filter()方法查找到多条内容
        if result:
            for r in result:  #循环添加匹配到的所有值
                event = {}
                event['name']      = r.name
                event['limit']     = r.limit
                event['status']    = r.status
                event['address']   = r.address
                event['start_time']= r.start_time
                datas.append(event)

            return JsonResponse({'status':200,'message':'success','data':datas})

        else:
            return JsonResponse({'status':10022,'message':'query result is empty'})


#嘉宾添加接口 测试通过
def add_guest(request):

    eid      =  request.POST.get('eid','')       #关联发布会id
    realname =  request.POST.get('realname','')  #姓名
    phone    =  request.POST.get('phone','')     #手机号
    email    =  request.POST.get('email','')     #邮箱

    if eid == '' or realname == '' or phone =='':

        return JsonResponse({'status':10021,'message':'parameter_error'})

    result = Event.objects.filter(id=eid)

    if not result:
        return JsonResponse({'status':10022,'message':'event id null'})

    result = Event.objects.get(id=eid).status


    if not result:
        return JsonResponse({'status':10023,
                             'message':'event status is not available'})


    event_limit = Event.objects.get(id=eid).limit    #发布会限制人数
    guest_limit = Guest.objects.filter(event_id=eid) #发布会已添加的嘉宾数

    print("已添加的嘉宾数",guest_limit)


    if len(guest_limit) >= event_limit:
        return  JsonResponse({'status':10024,'message':'event number is full'})

    event_time = Event.objects.get(id=eid).start_time    #发布会时间

    event_time = str(event_time).split("+")[0] #字符串时间日期格式例如:2017-05-22 10:27:18


    timeArray  = time.strptime(event_time,"%Y-%m-%d %H:%M:%S")  #将字符串时间类型转换为time类型
    # time.struct_time(tm_year=2017, tm_mon=5, tm_mday=22, tm_hour=10, tm_min=25, tm_sec=14, tm_wday=0, tm_yday=142, tm_isdst=-1) <class 'time.struct_time'>


    e_time     = int(time.mktime(timeArray))  #时间格式转换成int类型的时间戳例如:1495419914


    now_time   = str(time.time())   #当前时间

    ntime      = now_time.split(".")[0]
    n_time     = int(ntime) #时间戳(int)

    if n_time >= e_time:

        return JsonResponse({'status':10025,'message':'event has started'})

    try:
        Guest.objects.create(realname=realname,phone=int(phone),email=email,sign=0,event_id=int(eid))

    except IntegrityError:
        return JsonResponse({'status':10026,
                             'message':'the event guest phone number repeat'})

    return JsonResponse({'status':200,'message':'add guest success'})


#嘉宾查询接口 测试OK
def get_guest_list(request):

    eid   = request.GET.get("eid","")     #关联发布会id
    phone = request.GET.get("phone","")   #嘉宾手机号

    if eid == '':
        return  JsonResponse({'status':10021,'message':'eid cannot be empty'})

    if eid !='' and phone =='':
        datas = []
        results = Guest.objects.filter(event_id=eid)
        print("results eid",request)
        if results:
            for r in results:

                guest = {}
                guest['realname'] = r.realname
                guest['phone']    = r.phone
                guest['email']    = r.email
                guest['sign']     = r.sign

                datas.append(guest)

            return JsonResponse({"status":200,'message':'success','data':datas})

        else:
            return JsonResponse({'status':10022,'message':'query result is empty'})

    if eid !='' and phone !='':
        guest = {}

        try:
            result = Guest.objects.get(phone=phone,event_id=eid)

        except ObjectDoesNotExist:
            return JsonResponse({'status':10022,'message':'query result is empty'})
        else:
            guest['realname'] = result.realname
            guest['phone']    = result .phone
            guest['email']    = result .email
            guest['sign']     = result .sign

            return JsonResponse({'status':200,'message':'success','data':guest})


#嘉宾签到接口 测试ok
def user_sign(request):

    eid   = request.POST.get('eid','')    #发布会id
    phone = request.POST.get('phone','')  #嘉宾手机号


    if eid =='' or phone =='':
        return JsonResponse({'status':10021,'message':'parameter error'})

    result = Event.objects.filter(id=eid)

    if not result:
        return JsonResponse({'status':10022,'message':'event id null'})

    result = Event.objects.get(id = eid).status

    if not result:
        return JsonResponse({'status':10023,'message':'event status is not available'})

    event_time = Event.objects.get(id=eid).start_time    #发布会时间

    etime      = str(event_time).split("+")[0]


    timeArray  = time.strptime(etime,"%Y-%m-%d %H:%M:%S")

    e_time     = int(time.mktime(timeArray))  #将时间类型转换为整数

    now_time   = str(time.time())  #当前时间戳转换为str类型

    ntime      = now_time.split(".")[0] #保留整数位

    n_time     = int(ntime) #str类型转换int类型

    if n_time >= e_time:

        return JsonResponse({'status':10024,'message':'event has started'})

    result = Guest.objects.filter(phone = phone)

    if not result:

        return JsonResponse({'status':10025,'message':'user phone null'})

    result = Guest.objects.filter(event_id=eid,phone=phone)

    if not result:

        return JsonResponse({'status':10026,'message':'user did not participate in the conference'})

    result = Guest.objects.get(event_id=eid,phone=phone).sign

    if result:

        return JsonResponse({'status':10027,'message':'user has sign in'})

    else:
        Guest.objects.filter(event_id=eid,phone=phone).update(sign='1')
        return JsonResponse({'status':200,'message':'sign success'})

 PS:使用了方法介绍:

  • time.mktime()       time格式转换成时间戳             int(time.mktime())  时间戳转换成int类型 例如:1495419914
  • time.strptime(event_time,"%Y-%m-%d %H:%M:%S")  将str类型的日期时间,转换为time类型,格式time.struct_time(tm_year=2017, tm_mon=5, tm_mday=22,.....)
  • 查询数据库方法:get()精确查询只能查询一条,查询不到会报错,所以要抛异常;

                                      filter()可以查询多条,查询不到返回空list;

                                      filter(name__contains=name) 模糊匹配name,查询多条;

 2.2) sign/urls.py 添加api访问路径

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:tian

from django.conf.urls import url
from  sign import views_if

urlpatterns = [

    #guest system interface
    #ex :/api/add_event/
    url(r'^add_event/',views_if.add_event,name='add_event'),

    url(r'^add_guest/',views_if.add_guest,name="add_guest"),

    url(r'^get_event_list',views_if.get_event_list,name='get_event_list'),

    url(r'^get_guest_list',views_if.get_guest_list,name='get_guest_list'),

    url(r'^user_sign/',views_if.user_sign,name='user_sign'),
]

 guest/urls 添加二级路径

from django.conf.urls import url
from django.contrib import admin
from sign import views   #导入sign应用views文件
from django.conf.urls import  url,include
from sign import views_if  #配置二级路径

urlpatterns = [

    url(r'^admin/', admin.site.urls),

    url(r'^index/$',views.index), #添加index/路径

    url(r'^login_action/$',views.login_action),

    url(r'^event_manage/$',views.event_manage),

    url(r'^$',views.index),

    url(r'^accounts/login/$',views.index),

    url(r'^search_name/$',views.search_name),

    url(r'^guest__manage/$',views.guest__manage), #嘉宾路径

    url(r'^search_realname/$',views.search_realname),#嘉宾搜索路径

    # sign_index_action
    # http://127.0.0.1:8001/sign_index/1/

    url(r'^sign_index/(?P<event_id>[0-9]+)/$',views.sign_index),#跳转到签到页面路由

    url(r'^sign_index_action/(?P<event_id>[0-9]+)/$',views.sign_index_action),#输入手机号码后,点击签到跳转的路径

    url(r'^logout/$',views.logout),

    url(r'^api/',include('sign.urls',namespace="sign"))  #二级API路径

]

 

posted on 2018-03-23 10:20  赛兔子  阅读(1034)  评论(0)    收藏  举报

导航