Diango博客--23.单元测试:测试 blog 应用

文章目录

  • 1. 前言
  • 2. 搭建测试环境
  • 3. 测试模型
  • 4. 测试视图
  • 5. 测试模板标签
  • 6. 测试辅助方法和类

1. 前言

我们博客功能越来越来完善了,但这也带来了一个问题,我们不敢轻易地修改已有功能的代码了!

我们怎么知道代码修改后带来了预期的效果?万一改错了,不仅新功能没有用,原来已有的功能都可能被破坏。此前我们开发一个新的功能,都是手工运行开发服务器去验证,不仅费时,而且极有可能验证不充分。

如何不用每次开发了新的功能或者修改了已有代码都得去人工验证呢?解决方案就是编写自动化测试,将人工验证的逻辑编写成脚本,每次新增或修改代码后运行一遍测试脚本,脚本自动帮我们完成全部测试工作。

接下来我们将进行两种类型的测试,一种是单元测试,一种是集成测试。

单元测试是一种比较底层的测试,它将一个功能逻辑的代码块视为一个单元(例如一个函数、方法、或者一个 if 语句块等,单元应该尽可能小,这样测试就会更加充分),程序员编写测试代码去测试这个单元,确保这个单元的逻辑代码按照预期的方式执行了。通常来说我们一般将一个函数或者方法视为一个单元,对其进行测试。

集成测试则是一种更加高层的测试,它站在系统角度,测试由各个已经经过充分的单元测试的模块组成的系统,其功能是否符合预期。

我们首先来进行单元测试,确保各个单元的逻辑都没问题后,然后进行集成测试,测试整个博客系统的可用性。

Python 一般使用标准库 unittest 提供单元测试,django 拓展了单元测试,提供了一系列类,用于不同的测试场合。其中最常用到的就是 django.test.TestCase 类,博客应用的单元测试,主要就是和这个类打交道。这个类和 Python 标准库的 unittest.TestCase 类似,只是拓展了以下功能:

1)提供了一个 client 属性,这个 client 是 Client 的实例。可以把 Client 看做一个发起 HTTP 请求的功能库(类似于 requests),这样我们可以方便地使用这个类测试视图函数。

2)运行测试前自动创建数据库,测试运行完毕后自动销毁数据库。我们肯定不希望自动生成的测试数据影响到真实的数据。

django 应用的单元测试包括:

1)测试 model,model 的方法是否返回了预期的数据,对数据库的操作是否正确。

2)测试表单,数据验证逻辑是否符合预期

3)测试视图,针对特定类型的请求,是否返回了预期的响应

4)其它的一些辅助方法或者类等

接下来我们就逐一地来测试上述内容。

2. 搭建测试环境

测试写在 tests.py 里(应用创建时就会自动创建这个文件),首先来个冒烟测试,用于验证测试功能是否正常,在 blog\tests.py 文件写入如下代码:

from django.test import TestCaseclass SmokeTestCase(TestCase):def test_smoke(self):self.assertEqual(1 + 1, 2)

使用 manage.py 的 test 命令将自动发现 django 应用下的 tests 文件或者模块,并且自动执行以 test_ 开头的方法。运行:

pipenv run python manage.py test

在这里插入图片描述
OK 表明我们的测试运行成功。

不过,如果需要测试的代码比较多,把全部测试逻辑一股脑塞入 tests.py,这个模块就会变得十分臃肿,不利于维护,所以我们把 tests.py 文件升级为一个包,不同的单元测试写到包下对应的模块中,这样便于模块化地维护和管理。

删除 blog\tests.py 文件,然后在 blog 应用下创建一个 tests 包,再创建各个单元测试模块:

test_models.py 存放和模型有关的单元测试
test_views.py 测试视图函数
test_templatetags.py 测试自定义的模板标签
test_utils.py 测试一些辅助方法和类等
在这里插入图片描述
注意:tests 包中的各个模块必须以 test_ 开头,否则 django 无法发现这些测试文件的存在,从而不会运行里面的测试用例。

3. 测试模型

模型需要测试的不多,因为基本上都是使用了 django 基类 models.Model 的特性,自己的逻辑很少。拿最为复杂的 Post 模型举例,它包括的逻辑功能主要有:

1)str 方法,返回 title 用于模型实例的字符表示
2)save 方法中设置文章创建时间(created_time)和摘要(exerpt)
3) get_absolute_url 返回文章详情视图对应的 url 路径
4) increase_views 将 views 字段的值 +1

单元测试就是要测试这些方法执行后的确返回了上面预期的结果,我们在 test_models.py 中新增一个类,叫做 PostModelTestCase,在这个类中编写上述单元测试的用例。

from django.apps import appsclass PostModelTestCase(TestCase):def setUp(self):# 断开 haystack 的 signal,测试生成的文章无需生成索引apps.get_app_config('haystack').signal_processor.teardown()user = User.objects.create_superuser(username='admin', email='admin@hellogithub.com', password='admin')cate = Category.objects.create(name='测试')self.post = Post.objects.create(title='测试标题',body='测试内容',category=cate,author=user,)def test_str_representation(self):self.assertEqual(self.post.__str__(), self.post.title)def test_auto_populate_modified_time(self):self.assertIsNotNone(self.post.modified_time)old_post_modified_time = self.post.modified_timeself.post.body = '新的测试内容'self.post.save()self.post.refresh_from_db()self.assertTrue(self.post.modified_time > old_post_modified_time)def test_auto_populate_excerpt(self):self.assertIsNotNone(self.post.excerpt)self.assertTrue(0 < len(self.post.excerpt) <= 54)def test_get_absolute_url(self):expected_url = reverse('blog:detail', kwargs={'pk': self.post.pk})self.assertEqual(self.post.get_absolute_url(), expected_url)def test_increase_views(self):self.post.increase_views()self.post.refresh_from_db()self.assertEqual(self.post.views, 1)self.post.increase_views()self.post.refresh_from_db()self.assertEqual(self.post.views, 2)
  1. 这里代码虽然比较多,但做的事情很明确。setUp 方法会在每一个测试案例运行前执行,这里做的事情是在数据库中创建一篇文章,用于测试。

  2. 接下来的各个 test_* 方法就是对于各个功能单元的测试,以 test_auto_populate_modified_time 为例,这里我们要测试文章保存到数据库后,modifited_time 被正确设置了值(期待的值应该是文章保存时的时间)。

self.assertIsNotNone(self.post.modified_time) 断言文章的 modified_time 不为空,说明的确设置了值。TestCase 类提供了系列 assert* 方法用于断言测试单元的逻辑结果是否和预期相符,一般从方法的命名中就可以读出其功能,比如这里 assertIsNotNone 就是断言被测试的变量值不为 None。接着我们尝试通过:

self.post.body = '新的测试内容'
self.post.save()

修改文章内容,并重新保存数据库。预期的结果应该是,文章保存后,modifited_time 的值也被更新为修改文章时的时间,接下来的代码就是对这个预期结果的断言:

self.post.refresh_from_db()
self.assertTrue(self.post.modified_time > old_post_modified_time)

这个 refresh_from_db 方法将刷新对象 self.post 的值为数据库中的最新值,然后我们断言数据库中 modified_time 记录的最新时间比原来的时间晚,如果断言通过,说明我们更新文章后,modified_time 的值也进行了相应更新来记录修改时间,结果符合预期,测试通过。

  1. 其它的测试方法都是做着类似的事情,这里不再一一讲解,请自行看代码分析。

4. 测试视图

视图函数测试的基本思路是,向某个视图对应的 URL 发起请求,视图函数被调用并返回预期的响应,包括正确的 HTTP 响应码和 HTML 内容。

我们的博客应用包括以下类型的视图需要进行测试:

1)首页视图 IndexView,访问它将返回全部文章列表。
2)标签视图,访问它将返回某个标签下的文章列表。如果访问的标签不存在,返回 404 响应。
3)分类视图,访问它将返回某个分类下的文章列表。如果访问的分类不存在,返回 404 响应。
4)归档视图,访问它将返回某个月份下的全部文章列表。
5)详情视图,访问它将返回某篇文章的详情,如果访问的文章不存在,返回 404。
5)自定义的 admin,添加文章后自动填充 author 字段的值。
6)RSS,返回全部文章的 RSS 内容。

首页视图、标签视图、分类视图、归档视图都是同一类型的视图,他们预期的行为应该是:

1)返回正确的响应码,成功返回200,不存在则返回404。
2) 没有文章时正确地提示暂无文章。
3) 渲染了正确的 html 模板。
4) 包含关键的模板变量,例如文章列表,分页变量等。

我们首先来测试这几个视图。为了给测试用例生成合适的数据,我们首先定义一个基类,预先定义好博客的数据内容,其它视图函数测试用例继承这个基类,就不需要每次测试时都创建数据了。我们创建的测试数据如下:

  1. 分类一、分类二
  2. 标签一、标签二
  3. 文章一,属于分类一和标签一,文章二,属于分类二,没有标签
class BlogDataTestCase(TestCase):def setUp(self):apps.get_app_config('haystack').signal_processor.teardown()# Userself.user = User.objects.create_superuser(username='admin',email='admin@hellogithub.com',password='admin')# 分类self.cate1 = Category.objects.create(name='测试分类一')self.cate2 = Category.objects.create(name='测试分类二')# 标签self.tag1 = Tag.objects.create(name='测试标签一')self.tag2 = Tag.objects.create(name='测试标签二')# 文章self.post1 = Post.objects.create(title='测试标题一',body='测试内容一',category=self.cate1,author=self.user,)self.post1.tags.add(self.tag1)self.post1.save()self.post2 = Post.objects.create(title='测试标题二',body='测试内容二',category=self.cate2,author=self.user,created_time=timezone.now() - timedelta(days=100))

以 CategoryViewTestCase 为例

class CategoryViewTestCase(BlogDataTestCase):def setUp(self):super().setUp()self.url = reverse('blog:category', kwargs={'pk': self.cate1.pk})self.url2 = reverse('blog:category', kwargs={'pk': self.cate2.pk})def test_visit_a_nonexistent_category(self):url = reverse('blog:category', kwargs={'pk': 100})response = self.client.get(url)self.assertEqual(response.status_code, 404)def test_without_any_post(self):Post.objects.all().delete()response = self.client.get(self.url2)self.assertEqual(response.status_code, 200)self.assertTemplateUsed('blog/index.html')self.assertContains(response, '暂时还没有发布的文章!')def test_with_posts(self):response = self.client.get(self.url)self.assertEqual(response.status_code, 200)self.assertTemplateUsed('blog/index.html')self.assertContains(response, self.post1.title)self.assertIn('post_list', response.context)self.assertIn('is_paginated', response.context)self.assertIn('page_obj', response.context)self.assertEqual(response.context['post_list'].count(), 1)expected_qs = self.cate1.post_set.all().order_by('-created_time')self.assertQuerysetEqual(response.context['post_list'], [repr(p) for p in expected_qs])

这个类首先继承自 BlogDataTestCase,setUp 方法别忘了调用父类的 stepUp 方法,以便在每个测试案例运行时,设置好博客测试数据。

然后就是进行了3个案例测试:

  1. 访问一个不存在的分类,预期返回 404 响应码。

  2. 没有文章的分类,返回200,但提示暂时还没有发布的文章!渲染的模板为 index.html

  3. 访问的分类有文章,则响应中应该包含系列关键的模板变量,post_list、is_paginated、page_obj,post_list 文章数量为1,因为我们的测试数据中这个分类下只有一篇文章,post_list 是一个 queryset,预期是该分类下的全部文章,时间倒序排序。

其它的 TagViewTestCase 等测试类似,请自行参照代码分析。

博客文章详情视图的逻辑更加复杂一点,所以测试用例也更多,主要需要测试的点有:

  1. 访问不存在文章,返回404。
  2. 文章每被访问一次,访问量 views 加一。
  3. 文章内容被 markdown 渲染,并生成了目录。

测试代码如下:

class PostDetailViewTestCase(BlogDataTestCase):def setUp(self):super().setUp()self.md_post = Post.objects.create(title='Markdown 测试标题',body='# 标题',category=self.cate1,author=self.user,)self.url = reverse('blog:detail', kwargs={'pk': self.md_post.pk})def test_good_view(self):response = self.client.get(self.url)self.assertEqual(response.status_code, 200)self.assertTemplateUsed('blog/detail.html')self.assertContains(response, self.md_post.title)self.assertIn('post', response.context)def test_visit_a_nonexistent_post(self):url = reverse('blog:detail', kwargs={'pk': 100})response = self.client.get(url)self.assertEqual(response.status_code, 404)def test_increase_views(self):self.client.get(self.url)self.md_post.refresh_from_db()self.assertEqual(self.md_post.views, 1)self.client.get(self.url)self.md_post.refresh_from_db()self.assertEqual(self.md_post.views, 2)def test_markdownify_post_body_and_set_toc(self):response = self.client.get(self.url)self.assertContains(response, '文章目录')self.assertContains(response, self.md_post.title)post_template_var = response.context['post']self.assertHTMLEqual(post_template_var.body_html, "<h1 id='标题'>标题</h1>")self.assertHTMLEqual(post_template_var.toc, '<li><a href="#标题">标题</li>')

接下来是测试 admin 添加文章和 rss 订阅内容,这一块比较简单,因为大部分都是 django 的逻辑,django 已经为我们进行了测试,我们需要测试的只是自定义的部分,确保自定义的逻辑按照预期的定义运行,并且得到了预期的结果。

对于 admin,预期的结果就是发布文章后,的确自动填充了 author:

class AdminTestCase(BlogDataTestCase):def setUp(self):super().setUp()self.url = reverse('admin:blog_post_add')def test_set_author_after_publishing_the_post(self):data = {'title': '测试标题','body': '测试内容','category': self.cate1.pk,}self.client.login(username=self.user.username, password='admin')response = self.client.post(self.url, data=data)self.assertEqual(response.status_code, 302)post = Post.objects.all().latest('created_time')self.assertEqual(post.author, self.user)self.assertEqual(post.title, data.get('title'))self.assertEqual(post.category, self.cate1)
  1. reverse(‘admin:blog_post_add’) 获取 admin 管理添加博客文章的 URL,django admin 添加文章的视图函数名admin:blog_post_add,一般 admin 后台操作模型的视图函数命名规则是 <app_label>_<model_name> _<action。
  2. self.client.login(username=self.user.username, password=‘admin’) 登录用户,相当于后台登录管理员账户。
  3. self.client.post(self.url, data=data) ,向添加文章的 url 发起 post 请求,post 的数据为需要发布的文章内容,只指定了 title,body和分类。

接着我们进行一系列断言,确认是否正确创建了文章。

RSS 测试也类似,我们期待的是,它返回的内容中的确包含了全部文章的内容:

class RSSTestCase(BlogDataTestCase):def setUp(self):super().setUp()self.url = reverse('rss')def test_rss_subscription_content(self):response = self.client.get(self.url)self.assertContains(response, AllPostsRssFeed.title)self.assertContains(response, AllPostsRssFeed.description)self.assertContains(response, self.post1.title)self.assertContains(response, self.post2.title)self.assertContains(response, '[%s] %s' % (self.post1.category, self.post1.title))self.assertContains(response, '[%s] %s' % (self.post2.category, self.post2.title))self.assertContains(response, self.post1.body)self.assertContains(response, self.post2.body)

5. 测试模板标签

全部模板引擎的测试套路都是一样,构造需要的上下文,构造模板,使用上下文渲染模板,断言渲染的模板内容符合预期。以为例:

def test_show_recent_posts_with_posts(self):post = Post.objects.create(title='测试标题',body='测试内容',category=self.cate,author=self.user,)context = Context(show_recent_posts(self.ctx))template = Template('{% load blog_extras %}''{% show_recent_posts %}')expected_html = template.render(context)self.assertInHTML('<h3 class="widget-title">最新文章</h3>', expected_html)self.assertInHTML('<a href="{}">{}</a>'.format(post.get_absolute_url(), post.title), expected_html)

这个模板标签对应侧边栏的最新文章版块。我们进行了2处关键性的内容断言。一个是包含最新文章版块标题,一个是内容中含有文章标题的超链接。

这里测试的核心内容是,模板中 {% templatetag %} 被渲染成了正确的 HTML 内容。你可以看到测试代码中对应的代码:

context = Context(show_recent_posts(self.ctx))
template = Template('{% load blog_extras %}''{% show_recent_posts %}'
)
expected_html = template.render(context)

注意模板标签本质上是一个 Python 函数,第一句代码中我们直接调用了这个函数,由于它需要接受一个 Context 类型的标量,因此我们构造了一个空的 context 给它,调用它将返回需要的上下文变量,然后我们构造了一个需要的上下文变量。

接着我们构造了一个模板对象。

最后我们使用构造的上下文去渲染了这个模板。

我们调用了模板引擎的底层 API 来渲染模板,视图函数会渲染模板,返回响应,但是我们没有看到这个过程,是因为 django 帮我们在背后的调用了这个过程。

6. 测试辅助方法和类

我们的博客中只自定义了关键词高亮的一个逻辑。

class HighlighterTestCase(TestCase):def test_highlight(self):document = "这是一个比较长的标题,用于测试关键词高亮但不被截断。"highlighter = Highlighter("标题")expected = '这是一个比较长的<span class="highlighted">标题</span>,用于测试关键词高亮但不被截断。'self.assertEqual(highlighter.highlight(document), expected)highlighter = Highlighter("关键词高亮")expected = '这是一个比较长的标题,用于测试<span class="highlighted">关键词高亮</span>但不被截断。'self.assertEqual(highlighter.highlight(document), expected)

这里 Highlighter 实例化时接收搜索关键词作为参数,然后 highlight 将搜索结果中关键词包裹上 span 标签。

Highlighter 事实上 haystack 为我们提供的类,我们只是定义了 highlight 方法的逻辑。我们又是如何知道 highlight 方法的逻辑呢?如何进行测试呢?

我是看源码,大致了解了 Highlighter 类的实现逻辑,然后我从 haystack 的测试用例中找到了 highlight 的测试方法。

所以,有时候不要惧怕去看源代码,Python 世界里一切都是开源的,源代码也没有什么神秘的地方,都是人写的,别人能写出来,你学习后也一样能写出来。单元测试的代码一般比较冗长重复,但目的也十分明确,而且大都以顺序逻辑组织,代码自成文档,非常好读。

单纯看文章中的讲解你可能仍有迷惑,但是好好读一遍示例项目中测试部分的源代码,你一定会对单元测试有一个更加清晰的认识,然后依葫芦画瓢,写出对自己项目代码的单元测试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/452115.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图片格式转换工具与方法

2019独角兽企业重金招聘Python工程师标准>>> 使用ffmpeg进行格式转换 1.jpg 转 I420 ffmpeg -i 001.jpg -pix_fmt yuv420p 001_I420_fromJPG.yuv 2.png 转 I420 ffmpeg -i 222.png -pix_fmt yuv420p 222_I420_fromPNG.yuv 3.bmp 转 I420 ffmpeg -i xxx.bmp -pix_fmt…

Diango博客--24.单元测试:测试评论应用

文章目录1. 前言2. 数据基类3.测试 Comment Model4. 测试视图函数5. 测试模板标签1. 前言 comments应用的测试和blog应用测试的套路是一样的。 先来建立测试文件的目录结构。首先在 comments 应用的目录下建立一个名为 tests 的 Python 包&#xff0c;然后删除 comments 应用…

结合shiro 的图形验证码生成

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 在做用户登录功能时&#xff0c;很多时候都需要验证码支持&#xff0c;验证码的目的是为了防止机器人模拟真实用户登录而恶意访问&#…

E24- please install the following Perl modules before executing ./mysql_install_db

2019独角兽企业重金招聘Python工程师标准>>> [roott-cet7 scripts]# ./mysql_install_db --basedir/usr/local/mysql/ --datadir/app/data/ --usermysql FATAL ERROR: please install the following Perl modules before executing ./mysql_install_db: Data::Dumpe…

SpringMVC异常报406 (Not Acceptable)的解决办法

使用SpsringMVC&#xff0c;使用restEasy调试&#xff0c;controller请求设置如下&#xff1a; Java代码 RequestMapping(value"/list",methodRequestMethod.GET,producesMediaType.APPLICATION_JSON_VALUE) ResponseBody public List<EditTimeout> list()…

Diango博客--25.使用Coverage统计测试覆盖率

文章目录1. 前言2. 安装 Coverage3. 简单配置 Coverage4. 运行 Coverage5. 完善 Coverage 配置6. 生成 HTML 报告7. 完善单元测试1. 前言 我们完成了对 blog 应用和 comment 应用这两个核心 app 的测试。现在我们想知道的是究竟测试效果怎么样呢&#xff1f;测试充分吗&#x…

Android动画之逐帧动画(FrameAnimation)详解

今天我们就来学习逐帧动画,废话少说直接上效果图如下: 帧动画的实现方式有两种&#xff1a; 一、在res/drawable文件夹下新建animation-list的XML实现帧动画 1、首先在res/drawable文件夹下添加img00-img24共25张图片 2、新建frame_anim.xml [html] view plaincopy <?xml v…

网络爬虫--1.通用爬虫和聚焦爬虫

文章目录一.前言二.通用爬虫1.工作原理2.通用爬虫的局限性三.聚焦爬虫一.前言 根据使用场景&#xff0c;网络爬虫可分为 通用爬虫 和 聚焦爬虫 两种。 其中通用网络爬虫是捜索引擎抓取系统&#xff08;Baidu、Google、Yahoo等&#xff09;的重要组成部分。主要目的是将互联网…

敏捷教练的工具箱

学习并不是简简单单的阅读和浏览&#xff0c;而是一个积累的过程&#xff0c;一个通过持续的学习&#xff0c;对自己的知识体系不断丰富、索引的过程。接下来我会从四个方面入手分享我的经验。 高质量的信息源和高效的学习 Google是一个很好的工具&#xff0c;通过它&#x…

python 发送邮件的两种方式【终极篇】

python 发送邮件的两种方式【终极篇】 一&#xff0c;利用python自带的库 smtplib简单高效 from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.header import Header import smtplib from django.conf import settingsmail_hos…

网络爬虫--2.HTTP和HTTPS

文章目录一.简介二.HTTP的请求与响应三.客户端HTTP请求1.格式2.请求方法四.常用的请求报头1.Host (主机和端口号)2.Connection (链接类型)3.Upgrade-Insecure-Requests (升级为HTTPS请求)4. User-Agent (浏览器名称)5. Accept (传输文件类型)6.Referer (页面跳转处)7.Accept-En…

IBM王阳:软件是凝聚创新力的最佳平台

导读&#xff1a;在IBM全球副总裁兼IBM中国开发中心总经理王阳博士看来&#xff0c;IBM百年不衰的根本原因在于将创新力凝结成软件然后进行合适的传播&#xff0c;其间最重要的是成功打造出了一个吸引人才、培养研发人才并激发出人才创新力的环境和氛围。而保持创新领导力的关键…

Jquery 多行拖拽图片排序 jq优化

<!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title>jQuery图片拖动排序代码</title><style type"text/css">.item_container{position:relative;height:auto;overflow:hidden;} .item_content ul{li…

分享11款主流的开源编程工具

导读&#xff1a;有了开源编程工具&#xff0c;在基于开源许可证的情况下您可以轻松学习、修改、提高代码的质量&#xff0c;本文收集了11款最主流的且有价值的开源编程工具。或许会给您带来一丝惊喜。一起来看下吧。 NO.1 Rhomobile Rhodes Ruby或许是Github上第二大流行语言…

谁在告谁?移动专利混战图

移动领域激战正酣&#xff0c;同样是没有永远的朋友&#xff0c;只有永远的利益。 苹果刚刚起诉三星的Galaxy手机和平板电脑山寨了苹果的产品&#xff0c;而此前两家并没有过节。再比如微软和亚马逊以及HTC之间的授权协议争端。移动领域的争端如此之多&#xff0c;以至于看客无…

光棍节程序员闯关秀过关全攻略

maven/java/web/bootstrapQQ群&#xff1a;566862629。希望更多人一起帮助我学习。 光棍节程序员闯关秀过关全攻略。程序员的寂寞谁能懂?"SF光棍节程序员闯关秀"智力挑战小游戏火热上线&#xff0c;看看你能闯到第几关&#xff1f; 游戏地址: http://segmentfault…

jekins搭建

2019独角兽企业重金招聘Python工程师标准>>> 转自 https://www.cnblogs.com/hdwang/p/6081994.html &#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xff1d;&#xf…

块级元素的margin-left和margin-right的用法注意

此时是有效果显示的因为html文档流默认是从上往下&#xff0c;从左往右进行显示的&#xff0c;所以此时是有效果的。那如果此时把#son的块元素的margin-right:20px; 是没有效果的此时是没有效果的&#xff0c;如图所示&#xff1a;如果此时想要margin-right有效果的话&#xf…

Apache Tiles的基本使用

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 1、概述 对于一个新的技术&#xff0c;了解其基本的概念和和原理是学好该技术的基础。 2、Tiles的概念 Tiles 是复合视图模式&#xff0…

网络爬虫--6.urllib库的基本使用(2)

文章目录一. urllib.parse.urlencode()和urllib.parse.unquote()二. Get方式三. 批量爬取百度贴吧数据四.POST方式五.关于CA六.处理HTTPS请求 SSL证书验证一. urllib.parse.urlencode()和urllib.parse.unquote() 编码工作使用urllib.parse的urlencode()函数&#xff0c;帮我们…