XieJava's blog

记录最好的自己


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于

django实现开发、测试、生产环境配置区分

发表于 2024-09-16 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 1.2k | 阅读时长 ≈ 4

任何实际的软件项目中都要经过开发、测试、然后上生产的阶段,在开发、测试的过程中往往会要频繁的切换开发、测试、生产等不同的环境。每个环境的配置有可能不一样,本文介绍如何实现django项目配置环境变量实现开发、测试、生产灵活便捷的切换。

一、为什么要区分开发 (dev)、测试 (test) 和生产 (prod) 环境

在Django项目中区分开发、测试和生产环境是非常重要的,这主要是因为不同环境在多个方面存在显著差异。
● 开发环境:通常配置为易于开发和调试,比如开启调试模式(DEBUG = True),这样可以提供更详细的错误信息和页面跟踪,方便开发者定位问题。同时,开发环境可能使用本地数据库或内存数据库,以便快速启动和测试。
● 测试环境:配置为模拟生产环境,但用于自动化测试。测试环境需要确保测试的独立性和一致性,以便准确地评估应用程序在类似生产条件下的表现。测试环境通常使用与生产环境相同或相似的数据库设置,但数据是测试专用的。
● 生产环境:配置为高性能、高可用性和高安全性。关闭调试模式,优化数据库连接和缓存策略,确保应用程序能够处理大量并发请求和保障数据的安全。
通过区分这些环境,开发者可以更有效地管理django项目,确保每个环境都能满足其特定的需求,从而提高开发效率、保障数据安全和提升用户体验。

二、django项目如何通过配置实现环境配置的区分

对于django项目实现开发(dev)、测试(test)和生产(prod)环境的配置分离可以通过使用不同的设置文件实现。
下面,我们以一个实例来介绍在django的项目如何通过使用不同的设置文件的方式区分不同的环境。

1、针对不同的环境创建不同的设置文件settings.py

针对不同的环境创建不同的设置文件,如开发环境settings_dev.py ,测试环境setting_test.py,正式生产环境使用默认的setting.py,对于共性的配置可以抽取到base.py中,其他的设置文件可以继承base.py中的配置,只有实现各个环境特有的设置就可以了。
创建设置文件的目录结构如下图所示。

根据不同的环境创建不同的配置文件

2、在设置文件中根据需要进行配置区分

典型的,如开发环境settings_dev.py用的本地数据库并开启debug模式、测试环境setting_test.py用sqlite3数据库开启debug模式。
注意:其他的设置文件要继承base.py中的配置,需要加入from .base import *
参考配置如下:
setting_test.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from .base import *

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'test_db.sqlite3'),
}
}

setting_dev.py

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

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['127.0.0.1', 'localhost','ishareblog.com']

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'ishareblog_test',
'USER': 'ishareblog',
'PASSWORD': 'yourpassword',
'HOST': '127.0.0.1',
'PORT': '3306',
# 1、取消外键约束,否则多对多模型迁移报django.db.utils.IntegrityError: (1215, 'Cannot add foreign key constraint');
# 2、解决字符串4个字节的utf8编码的字符报错的问题
'OPTIONS': {
'charset': 'utf8mb4',"init_command": "SET foreign_key_checks = 0;",
}
}
}

3、根据不同的环境运行使用不同的设置文件

不同的环境有了不同的设置文件后,就可以在不同的环境运行和使用配置文件。
如生产环境使用的wsgi启动的服务,可以在wsgi.py文件中指定使用生产环境的配置文件

wsgi中使用生产环境配置文件

在开发环境中使用manage.py启动服务的,可以修改manage.py将设置文件改为setting_dev
开发环境在manage文件中使用开发设置文件

也可以通过命令行指定设置文件 python manage.py runserver --settings=ishareblog.settings_dev
如果时通过IDE启动,可以在IDE中指定使用的环境设置文件。
如下图所示:

在IDE中指定环境设置文件

如要使用测试环境的设置文件,可以配置成ishareblog.settings_test,或通过命令行指定设置文件 python manage.py runserver --settings=ishareblog.settings_test

至此,本文详细的介绍了通过不同的设置文件来区分配置开发、测试、生产环境的配置及如何根据不同的环境使用不同的设置文件。解决不同环境去频繁修改settings.py环境设置的问题。


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注:微信公众号,一起学习成长!

vue3+elementplus的表格展示和分页实战

发表于 2024-09-02 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 1.6k | 阅读时长 ≈ 8

Element Plus 是一个基于 Vue 3 的现代化 UI 组件库,旨在帮助开发者快速构建美观且功能丰富的 Web 应用程序。它提供了大量的 UI 组件,如按钮、表单、表格、弹出框、标签页、树形控件等,涵盖了 Web 应用开发中常见的大多数场景。本文通过一个实例来说明vue3+elementplus查询、展示和分页实战。

一、Element Plus的安装使用

要开始使用 Element Plus,首先需要在项目中安装它。如果你正在使用 Vue 3 的项目,可以通过 npm 或 yarn 安装 Element Plus:

1
npm install element-plus

然后可以在Vue 项目中全局引入 Element Plus:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createApp } from 'vue'
import App from './App.vue'

// 导入路由
import Router from './components/tools/Router'
// 导入ElementPlus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

const app = createApp(App)
// 遍历ElementPlusIconsVue中的所有组件进行祖册
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
// 向应用实例中全局注册图标组件
app.component(key, component)
}
app.use(ElementPlus) // 使用ElementPlus
app.use(Router); // 使用路由
app.mount('#app')

二、el-table 表格组件

el-table 是Element Plus 中的一个重要组件,用于展示列表数据。可以通过 组件来定义表格中的每一列,包括列标题、列宽、对齐方式等,可以结合 el-pagination 可以实现分页功能。示例代码如下:

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
<el-table
ref="multipleTable"
:data="postList"
tooltip-effect="dark"
style="width: 100%"
fit
:pagination="pagination"
@selection-change="handleSelectionChange" >
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
label="ID"
width="100"
prop="id">
</el-table-column>
<el-table-column
label="标题"
width="450"
prop="title">
</el-table-column>
<el-table-column
label="是否置顶"
width="100"
prop="isTop">
</el-table-column>
<el-table-column
label="热度"
width="100"
prop="viewsCount">
</el-table-column>
<el-table-column
label="发布时间"
width="200"
prop="pubTime">
</el-table-column>
<el-table-column
label="操作"
>
<template #default="scope">
<el-button size="mini" type="danger" @click="deleteItem(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>

其中
:data="postList" 绑定要显示在表格中的数据源,通常是一个对象数组
fit: 让表格宽度自动填充父容器。
:pagination="pagination" 绑定分页的数据对象

数据定义如下:

1
2
3
4
5
6
7
8
9
// 博客文章列表数据
postList:[],
// 分页
pagination: {
currentPage: 1, // 当前页
pageSize: 10, // 每页显示条数
total: 0, // 总条数
layout: 'total,sizes,prev, pager, next, jumper', // 分页布局
},

三、el-pagination 分页组件

el-pagination Element Plus 中用于实现分页功能的重要组件。它可以与 el-table 组件结合使用,实现数据的分页显示。示例代码如下:

1
2
3
4
5
6
7
8
9
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 30, 40]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>

属性
● @size-change="handleSizeChange" 当每页显示数量变化时触发。
● @current-change="handleCurrentChange" 当当前页变化时触发。
● :current-page="currentPage" 设置当前页。
● :page-sizes="[10, 20, 30, 40]" 设置每页可选的数量。
● :page-size="pageSize" 设置每页显示的数量。
● layout="total, sizes, prev, pager, next, jumper" 设置分页布局。
● :total="tableData.length" 设置总数据量。
方法:
● handleSelectionChange(val) 处理行选择变化。
● deleteItem(index) 删除指定行。
● handleSizeChange(val) 处理每页显示数量变化。
● handleCurrentChange(val) 处理当前页变化。

四、全部代码

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<template>
<div class="content-container" direction="vertical">
<!-- input -->
<div>
<el-container class="content-row">
<div class="input-tip">
文章标题:
</div>
<div class="input-field" style="width: 400px;">
<el-input v-model="queryParam.words"></el-input>
</div>
<el-button type="primary" @click="getBlogList">筛选</el-button>
<el-button type="danger" @click="clear">清空筛选</el-button>
</el-container>
</div>
<!-- list -->
<div>
<el-tabs type="card" @tab-click="handleClick">
<el-tab-pane label="全部"></el-tab-pane>
<el-tab-pane v-for="(item,index) in blogCategorys"
:key="index"
:label="item.title"
:name="item.id">
</el-tab-pane>
</el-tabs>
<el-table
ref="multipleTable"
:data="postList"
tooltip-effect="dark"
style="width: 100%"
fit
:pagination="pagination"
@selection-change="handleSelectionChange" >
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
label="ID"
width="100"
prop="id">
</el-table-column>
<el-table-column
label="标题"
width="450"
prop="title">
</el-table-column>
<el-table-column
label="是否置顶"
width="100"
prop="isTop">
</el-table-column>
<el-table-column
label="热度"
width="100"
prop="viewsCount">
</el-table-column>
<el-table-column
label="发布时间"
width="200"
prop="pubTime">
</el-table-column>
<el-table-column
label="操作"
>
<template #default="scope">
<el-button size="mini" type="danger" @click="deleteItem(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pagination.currentPage"
:page-sizes="[10, 20, 30, 40]"
:page-size="pagination.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total">
</el-pagination>
</div>
</div>
</div>
</template>

<style scoped>
.pagination-container {
margin-top: 20px;
text-align: center;
}
</style>

<script>
import {getBlogList,getBlogCategory} from '@/api'
export default {
data() {
return {
// 博客文章列表数据
postList:[],
// 筛选博客的参数
queryParam:{
words:"",
cateid:"",
tag:"",
search:"",
page:1,
size:10
},
// 分页
pagination: {
currentPage: 1, // 当前页
pageSize: 10, // 每页显示条数
total: 0, // 总条数
layout: 'total,sizes,prev, pager, next, jumper', // 分页布局
},
// 博客分类
blogCategorys:[],
// 当前选中的博客分类
selectCategory:"",
// 当前选中的博客文章
multipleSelection:[]
}
},
mounted () {
this.getBlogList();
this.getBlogCategory();
},
// 路由更新时刷新数据
beforeRouteUpdate (to) {
this.getBlogList();
this.getBlogCategory();
},
methods : {
// 获取博客文章列表数据
getBlogList() {
getBlogList(this.queryParam).then(res => {
this.postList = res.data.items
this.pagination.total = res.data.total
this.pagination.currentPage= res.data.page
console.log(res.data)
}).catch(err => {
console.log(err)
})
},
// 获取博客分类数据
getBlogCategory() {
getBlogCategory().then(res => {
this.blogCategorys = res.data
console.log(res)
}).catch(err => {
console.log(err)
})
},
// 改变分页大小
handleSizeChange(val) {
this.pagination.pageSize = val;
this.queryParam.size = val;
this.getBlogList();
},
// 跳到当前页
handleCurrentChange(val) {
this.pagination.currentPage = val;
this.queryParam.page = val;
this.getBlogList();
},
// 切换Tab 刷新数据
handleClick(tab) {
this.queryParam.cateid = tab.props.name
this.getBlogList();
},
// 清空筛选项
clear() {
this.queryParam.words=""
this.getBlogList();
},

}
}
</script>

五、效果

elementplus表格
表格展示及数据分页是前端开发常用的功能,通过vue3+elementplus能够快速是实现对数据的展示及分页。


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注微信公众号,一起学习、成长!

vue3+vite配置环境变量实现开发、测试、生产的区分

发表于 2024-08-25 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 973 | 阅读时长 ≈ 3

在vue的实际项目中都要经过开发、测试、然后上生产的阶段,在开发、测试的过程中往往会要频繁的切换开发、测试、生产等不同的环境。每个环境的配置有可能不一样,本文介绍如何通过vue3+vite配置环境变量实现开发、测试、生产灵活便捷的切换。

一、为什么需要区分 (dev)、测试 (test) 和生产 (prod) 环境

做过大型项目开发的都知道,每个项目都会要经历开发、测试、再到生产上线,一般在开发时候最常用到(development)开发环境、(production)生产环境、(test)测试环境。每个环境的配置可能都不太一样。

  • 开发环境:为开发人员提供一个安全的地方来进行编码和调试,不会影响到其他环境,一般来说开发人员在本地机器上运行和测试应用程序。
  • 测试环境:用于测试,模拟生产环境,确保新功能在部署前能够正常工作,并且不会影响现有功能。
  • 生产环境:是面向用户的最终环境,任何更改都必须经过严格的测试才能部署到这里,通常具有优化和最少的日志记录。

区分开发 (dev)、测试 (test) 和生产 (prod) 环境是软件开发中的一个最佳实践。不同的环境通常有不同的资源配置。典型的如vue所调用的后台接口数据,在开发平台可能是本地服务提供的接口、用于自动化测试可能是mock提供的数据、生产应该是正式环境提供的真实接口。

二、vue3的项目如何通过配置方式区分不同的环境

vue3的项目可以通过vite的环境变量配置来进行不同环境的配置,可以参考vite的官方文档《环境变量和模式》
接下来,我们以一个实例来介绍在vue3的项目如何通过vite配置方式区分不同的环境。

1、创建不同环境的.env文件

在vue3根目录下创建三个文件分别为.env.develoment、.env.test、.env.production
创建的文件需要.env开头

env文件

2、在不同的.env文件中配置相应的环境变量

1).env.develoment

1
2
3
NODE_ENV='development'
# API URL
VITE_APP_API_URL='http://localhost:8000'

2).env.test

1
2
3
NODE_ENV='test'
# API URL
VITE_APP_API_URL=''

3).env.production

1
2
3
NODE_ENV='production'
# API URL
VITE_APP_API_URL='http://iblog.ishareread.com'

这里我们主要通过VITE_APP_API_URL变量来区分不同的环境调用不同的接口,在开发环境调用本地接口http://localhost:8000 ,在测试环境用mock接口,在生产环境调用真实接口。

3、在项目中使用环境变量

使用import.meta.env.VITE_APP_API_URL在axios请求中使用环境变量的配置来调用不同的接口。

1
2
3
4
5
6
7
8
import axios from 'axios'
const api_rul = import.meta.env.VITE_APP_API_URL
// create an axios instance
const service = axios.create({
baseURL: api_rul,
timeout: 5000 // request timeout
})
export default service

查看调用的哪些环境变量,可以在在main.js打印console.log('环境变量:', import.meta.env);看一下
main.js

1
2
3
4
5
6
7
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'

console.log('环境变量:', import.meta.env);

createApp(App).mount('#app')

4、在package.json中定义运行项目的脚本命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"name": "mocktest",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"test": "vite --mode test",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.5",
"vue": "^3.4.29"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"mockjs": "^1.1.0",
"vite": "^5.3.1",
"vite-plugin-mock": "^3.0.2"
}
}

package.json

三、运行效果

开发环境运行npm run dev

npm run dev

测试环境运行npm run test

npm run dev

可以看出通过运行不同的命令通过环境变量区分了不同的运行环境,避免了不同环境去改代码去适配不同的环境。


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注微信公众号,一起学习、成长!

vue3+vite+axios+mock从接口获取模拟数据实战

发表于 2024-08-24 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 1.6k | 阅读时长 ≈ 7

在用Vue.js开发前端应用时通常要与后端服务进行交互,例如通过API接口获取数据,在后端服务接口还没有具备之前,可以通过mock(模拟)数据来进行开发。使用mock数据可以让前端开发人员独立于后端开发人员工作,加快开发速度。在没有真实数据的情况下,mock数据可以帮助开发者更快地看到UI的呈现效果和交互逻辑。

本文通过vue3+vite+axios+mock来介绍如何实现Vue.js的前端应用从接口获取模拟数据。

一、安装相关组件

1
2
npm install axios -S
npm install mockjs vite-plugin-mock -D

其中axios 是一个基于 Promise 非常强大且灵活的 HTTP 客户端,适用于 Vue.js 应用程序中的数据获取和后端交互。它可以简化 HTTP 请求的处理,并提供丰富的功能来满足不同的需求。我们用axios来实现与接口服务的http请求。

Mock.js 是一个用于生成随机数据的 JavaScript 库,它主要用于前端开发过程中模拟后端接口数据。Mock.js 提供了一套简洁易用的 API,可以帮助开发者快速生成符合特定规则的假数据,从而在没有后端支持的情况下进行前端开发和测试。

vite-plugin-mock 是一个专为 Vite 设计的插件,用于在 Vite 项目中模拟数据。它简化了使用 Mock.js 的过程,让开发者能够更加方便地管理模拟数据。

简单来说,就是mock.js提供mock数据,通过vite-plugin-mock,将管理mock发布成服务,通过axios通过http请求接口的方式获取mock数据。

安装相关组件后,在package.json中看到相关的组件信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package.json
{
"name": "mocktest",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"test": "vite --mode test",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.5",
"vue": "^3.4.29"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"mockjs": "^1.1.0",
"vite": "^5.3.1",
"vite-plugin-mock": "^3.0.2"
}
}

二、在vite.config.js中配置vite-plugin-mock插件

● viteMockServe的相关配置

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
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'

// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// mock 数据的 dev环境
viteMockServe({
// supportTs: true, // 是否开启支持ts
mockPath: 'mock', // 设置mockPath为根目录下的mock目录
localEnabled: true, // 设置是否监视mockPath对应的文件夹内文件中的更改
prodEnabled: false, // 设置是否启用生产环境的mock服务
watchFiles: true, // 是否监视文件更改
logger: true //是否在控制台显示请求日志
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

在viteMockServe中指定了mockPath为mock也就是根目录下的mock目录,在该目录下的mock服务都会被发布成mock服务。

三、实现mock服务

在根目录下新建mock目录在mock目录下新建mock文件实现mock服务,如app.js、user.js

mock目录

app.js代码如下:

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
export default [{
url: '/mock/api/getApiInfo',
method: 'get',
response: () => {
return {
code: 200,
title: 'mock api test.'
}
}
},
{
url: '/api/category',
type: 'get',
response: () => {
return {
code: 200,
data: [
{
id: 1,
title: 'JAVA',
href: '/category/java'
},
{
id: 2,
title: 'SpringBoot',
href: '/category/SpringBoot',
},
{
id: 3,
title: 'MySql',
href: '/category/MySql'
},
{
id: 4,
title: '随笔',
href: '/category/live'
}
]
}
}
}
]

在app.js中我们并没有用mock生产数据,只是实现了mock服务放到了mock文件目录通过viteMockServe发布出来,后面可以通过axios调用获取。
user.js代码如下:

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
import Mock from 'mockjs';

// 通过Mock生成模拟数据
const userdata = Mock.mock({
'list|10': [
{
'id|+1': 1,
'name': '@cname',
'age|18-60': 1,
'email': '@email',
},
],
});

export default [
{
url: '/mock/api/getUserInfo',
method: 'get',
response: () => {
return {
code: 200,
data: userdata
}
}
},
]

在user.js中,我们通过Mock生成模拟的用户列表数据

四、调用api接口请求mock数据

方法一、直接使用axios 请求mock 数据

1
import axios from 'axios'

在方法中通过axios.get()方法直接获取请求数据

1
2
3
4
5
6
7
async getData() {
await axios.get('/mock/api/getApiInfo').then(res =>{
console.log(res.data)
this.msg = res.data.title
}
)
},

方法二、对axios进行封装统一请求mock数据

建立一个service.js对axios进行封装,让后通过service.js来统一请求mock数据,这样做的好处是在切到真实接口的时候可以更加灵活
service.js的代码如下

1
2
3
4
5
6
7
8
9
import axios from 'axios'
const api_rul = '' //mock 接口地址可以为空字符串,真实接口配置为真实的接口地址。
// create an axios instance
const service = axios.create({
baseURL: api_rul,
timeout: 5000 // request timeout
})

export default service

通过一个统一的调用接口文件请求mock数据
如mockapi.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import service from '@/utils/service'

export function getCategory() {
return service({
url: '/api/category',
method: 'get',
params: {}
})
}

export function getUserInfo() {
return service({
url: '/mock/api/getUserInfo',
method: 'get',
params: {}
})
}

在methods中进行方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 方法二:通过封装后的get方法获取数据
getUserInfo()
{
getUserInfo().then(res =>{
console.log(res.data)
this.userinfo = res.data.data
}
)
},
getCategory()
{
getCategory().then(res =>{
console.log(res.data)
this.categorys = res.data.data
}
)
}

在vue的组件中具体的调用和展示代码如下:
HelloWorld.vue

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
<template>
<h1>{{ msg }}</h1>
<div>
人员列表
</div>
<div>
<ul>
<li v-for="(user) in userinfo.list" :key="index">
{{ user.id }} : {{ user.name }} {{ user.age }} {{ user.email }}
</li>
</ul>
</div>
<div>
目录
</div>
<div>
<ul>
<li v-for="(category) in categorys" :key="index">
{{ category.id }} : {{ category.title }} {{ category.href }}
</li>
</ul>
</div>
</template>
<script >
import axios from 'axios'
import { getCategory,getUserInfo } from '../api/mockapi'

export default {
data() {
return {
msg: 'Welcome to Your Vue.js App',
userinfo: {},
categorys: []
};
},
mounted() {
this.getData()
this.getUserInfo()
this.getCategory()
},
methods: {
// 方法一:直接axios请求调用获取mock数据
async getData() {
await axios.get('/mock/api/getApiInfo').then(res =>{
console.log(res.data)
this.msg = res.data.title
}
)
},
// 方法二:通过封装后的get方法获取数据
getUserInfo()
{
getUserInfo().then(res =>{
console.log(res.data)
this.userinfo = res.data.data
}
)
},
getCategory()
{
getCategory().then(res =>{
console.log(res.data)
this.categorys = res.data.data
}
)
}
}
}
</script>

整个工程的目录结构说明如下:

mocktest工程目录

五、实际运行效果

mock效果
可以看到分别用两种方式获取mock数据的效果,其中人员列表中的数据是mock生成的模拟数。


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注微信公众号,一起学习、成长!

不忘初心,方得始终,码农实现自我提升的心法与工具

发表于 2024-08-20 | 更新于: 2025-07-11 | 分类于 人生 | | 阅读次数:
字数统计: 1.7k | 阅读时长 ≈ 5

在信息技术飞速发展的今天,越来越多的人进入到IT行业,也使得这个行业越来越卷。随着信息化普及进入尾声以及全球经济下行,IT行业也像传统的房地产行业一样哀鸿遍野,码农也像农民工一样转行的转行失业的失业。想要在这个行业里站住脚不被淘汰只能是不断的提升学习提升自己的能力,让自己成为不可替代才行。

作为码农,如是今还有份稳定的工作,应该来说是幸运的,但也是很焦虑的。一方面不知道自己所负责的业务还能撑多久,公司还能撑多久;一方面还得要思考一旦失业不知道自己的技术水平还能不能找到合适的工作。作为社会人,只有有了对抗风险的本钱后,才会有底气,有了底气后才会不至于那么的焦虑。而底气来自两个方面一方面是有足够的钱;另一方面是有足够的能力。如果有足够的钱,像文艺复兴时期的大咖们被人包养,吃喝不愁,那我们可以不用焦虑,惬意的写代码那一定是很开心的;如果能力足够强,其实也不用担心,换个地方施展自己的能力而已。而我们的现状是既要幸幸苦苦赚钱维持自己的生活又要不断的学习来提升自己的能力适应这个不断发展的行业。

如何在繁忙琐碎的编码工作与个人成长之间找到平衡,下面以我个人的经历来谈谈我的思考。对于码农来说,面临最大的两个问题是如何更好的利用时间和如何通过学习来提升自我的问题。

一、如何更好的利用时间?

在李笑来老师的《和时间做朋友》的书中讲到,其实时间是不可以管理的,每个人每天只有24小时,能够管理的是自己的精力,也就是把自己的关注力放到哪里。要学习,就应该把学习精力、注意力投入足够多的时间。对于我们来说最大的挑战来自手机,下班后很多同学报复性的休息放松,想起自己上班工作了,休息的时候一定得要彻底的放松,拿着手机刷抖音、打游戏、聊天不知不觉就到了深夜。为了避免长时间的玩手机,我的做法是到了一定时间把手机所有的应用都关闭,给自己一段没有手机打挠的时间,很多手机都提供的“禅定模式”,开启“禅定模式”后手机上除了接打电话,其他的应用都暂时不能用,除非退出“禅定模式”,通过这种方式让自己的注意力从手机上移开,放到看书和学习上来。在这里我常用的专注力APP是”Forest 专注森林“,它可以设置一段专注力时间,比如60分钟,如果60分钟你没有动手机就成功的种下了一颗健康的树,如果中途心痒痒玩了手机这个树就会蔫掉。可以按时间周期统计你的专注的时间。

Forest 专注森林

当然专注力是一方面,其实更重要的是我们应该有自控力的意识,有自我提升学习的意识。有了意识以后才会有行动。

二、如何通过学习来提升自我?

焦虑是因为面对残酷的社会竞争压力以及对自身能力的不自信。所以很多东西都想学,机器学习、网络安全、英语、写作等等。正因为焦虑,所以面临的困惑是什么都想学,什么都想学的结果是什么都没有学会。相信大多数人都有这样的经历,心想着要不断的提升自己,桌上摆了很多书,今天看几页这本书,明天翻几页那本书,看上去天天在看书,实际上一本书都没有看进去。看英语的时候在想应该多花点时间看看专业书,看专业书的时候在想英语也很重要,要不看会英语。实际就是有限的时间精力和无限的需要学习的知识之间的矛盾。尤其是专业领域,看了很多书,不能学以致用,看了就忘,感觉就是学了个寂寞。

早几年我也有相同的困惑,认识到学习,目标非常重要,没有明确目标的学习到头来都是浪费时间。于是写了一篇《你有了一个目标,于是你有了一堆技能,而不是反过来》。其实道理大家都懂,要有目标,不忘初心,方得始终。但是对于个人来说目标其实并不好确定,大部分人来说其实也没有什么明确的目标,我的经验是在没有明确的目标的时候就把考证当做目标,以考促学。见《工作这么多年了,我为什么还在考证》。后来我意识到自我提升最核心的是要构建自己知识体系,我的目标是构建了自己的知识体系,能够利用自己的知识体系具备“成事”的能力。如何建立个人知识体系,借用网上的一张图。

个人知识体系

有了构建自己知识体系的大目标后,我把自己近几年需要巩固提升的方面做了一个学习地图,有了学习地图就像航海的地图一样,避免东一下西一下迷失方向。

学习地图

刘未鹏在他的《暗时间》中有个观点让我感触特别的深刻,意思是你所懂得的多少并不是在于你看了多少书而是取决于你思考有多深。“教是最好的学”、“书写是更好的思考”。为了践行“问题->输入->内化->输出”,笃信写是为了更好的思考,坚持写作,力争更好的思考。我开启了自己的博客持续在CSDN和自己的博客上进行写作输出,将自己碰到的问题、经验、思考写出来,即帮助了大家又促进了自己,希望能和大家一起成长。


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注:微信公众号,一起学习成长!

node20+版本下hexo部署报错失败的解决办法

发表于 2024-08-15 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 864 | 阅读时长 ≈ 4

最近升级了node.js的版本,升到了最新的稳定版本v20.16.0,结果发现在该版本下hexo部署报错失败。本文记录了node20+版本下hexo部署报错失败的解决办法。

一、报错信息

执行hexo的deploy部署命令

1
hexo d

具体报错信息如下

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
INFO  Deploying: git
INFO Clearing .deploy_git folder...
INFO Copying files from public folder...
FATAL Something's wrong. Maybe you can find the solution here: https://hexo.io/docs/troubleshooting.html
TypeError: mode must be int32 or null/undefined
at copyFile (node:fs:3020:11)
at tryCatcher (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\util.js:16:23)
at ret (eval at makeNodePromisifiedEval (C:\Users\xiejava\AppData\Roaming\npm\node_modules\hexo-cli\node_modules\bluebird\js\release\promisify.js:184:12), <anonymous>:13:39)
at D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\hexo-fs\lib\fs.js:144:39
at tryCatcher (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\util.js:16:23)
at Promise._settlePromiseFromHandler (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:547:31)
at Promise._settlePromise (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:604:18)
at Promise._settlePromise0 (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:649:10)
at Promise._settlePromises (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:729:18)
at Promise._fulfill (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:673:18)
at Promise._resolveCallback (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:466:57)
at Promise._settlePromiseFromHandler (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:559:17)
at Promise._settlePromise (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:604:18)
at Promise._settlePromise0 (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:649:10)
at Promise._settlePromises (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:729:18)
at Promise._fulfill (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:673:18)
at Promise._resolveCallback (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:466:57)
at Promise._settlePromiseFromHandler (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:559:17)
at Promise._settlePromise (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:604:18)
at Promise._settlePromise0 (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:649:10)
at Promise._settlePromises (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:729:18)
at Promise._fulfill (D:\CloudStation\personal\xiejavablog\myhexo\myblog\node_modules\bluebird\js\release\promise.js:673:18)

遇到FATAL Something's wrong TypeError: mode must be int32 or null/undefined这类错误通常是在使用Hexo生成静态文件时出现的。这个问题通常与Hexo的一些插件不兼容或配置不当有关。

二、解决办法

一般来说因为node.js和hexo存在版本适配的关系,所以当可能是一个版本过高一个版本过低导致的出现问题。
解决这个问题可以降低node.js的版本,可以将hexo更新至适配的高版本。这里介绍如何将hexo更新至适配的高版本。

1、查看哪些包需要更新

1
npm outdated

在这里插入图片描述

通过npm outdated 列出了所有需要升级的组件包,这里还给出了npm也需要升级的信息,需要将npm从10.8.1升级到10.8.2,具体的升级命令是 npm install -g npm@10.8.2
执行 npm install -g npm@10.8.2 完成npm的升级

2、升级更新需要升级的包

升级hexo和hexo-cli

1
npm update hexo-cli hexo --latest

在这里插入图片描述

重新执行npm outdated
可以看到hexo 的版本从4.2.0升级到了4.2.1
在这里插入图片描述

重新执行hexo d,发现不报错了,并且可以成功部署。
这里还有hexo-abbrlink和hexo-generator-search 不是适配的版本,虽然不影响hexo部署,但也可以用同样的方法将其升为最新适配的版本。

1
npm update hexo-abbrlink hexo-generator-search --latest

在这里插入图片描述


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注微信公众号,一起学习、成长!

django+markdown2+pygments实现markdown解析及代码高亮

发表于 2024-08-14 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 1.1k | 阅读时长 ≈ 4

随着markdown的流行,web应用系统常常会要碰到有使用markdown编辑器进行富文本编辑,然后在前台web页面进行显示。常见的博客系统当然也需要支持markdown的编辑与显示。本文就通过一个真实的博客系统来说明django+markdown2+pygments实现markdown解析及代码高亮。

一、后台管理支持markdown编辑

django应用的后台管理支持markdown可以用django-mdeditor,它是一个Django应用,它集成了markdown-editor,允许你在Django项目中使用富文本编辑器编写Markdown格式的内容。这个插件通常用于博客、论坛或任何需要用户输入Markdown文本的场景。

1、安装依赖

首先,通过pip安装django-mdeditor

1
pip install django-mdeditor

2、添加应用到INSTALLED_APPS

在settings.py文件中,将django_mdeditor添加到INSTALLED_APPS列表中,参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
INSTALLED_APPS = [
'blog',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'django_filters', # 注册条件查询
'mdeditor', # 注册markdown的应用
'drf_yasg2', # 接口文档
]

3、设置MEDIA_URL和MEDIA_ROOT

django-mdeditor使用文件上传功能,因此需要在settings.py中正确设置

1
2
3
MEDIA_URL和MEDIA_ROOT:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')

4、在模型中使用MdEditorField

在Django模型中,使用MdEditorField替换标准的TextField:

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
'''博客文章'''
class BlogPost(BaseModel):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=200, verbose_name='文章标题', unique = True)
category = models.ForeignKey(BlogCategory, blank=True,null=True, verbose_name='文章分类', on_delete=models.DO_NOTHING)
isTop = models.BooleanField(default=False, verbose_name='是否置顶')
isHot = models.BooleanField(default=False, verbose_name='是否热门')
isShow = models.BooleanField(default=False, verbose_name='是否显示')
summary = models.TextField(max_length=500, verbose_name='内容摘要', default='')
content = MDTextField(verbose_name='内容')
viewsCount = models.IntegerField(default=0, verbose_name="查看数")
commentsCount = models.IntegerField(default=0, verbose_name="评论数")
tags = models.ManyToManyField(to=Tag, related_name="tag_post", blank=True, default=None, verbose_name="标签")
blogSource = models.CharField(max_length=200, blank=True, null=True, default='',verbose_name='文章来源')
pubTime = models.DateTimeField(blank=True, null=True, verbose_name='发布日期')

@property
def tag_list(self):
return ','.join([i.tag for i in self.tags.all()])

def __str__(self):
return self.title

class Meta:
verbose_name = '博客文章'
verbose_name_plural = '博客文章'

在博客文章的模型中,文章内容需要支持markdown,所以content = MDTextField(verbose_name='内容')

5、查看效果

用django自带的后台管理admin就可以看到效果了

django自带的后台管理admin

二、前台支持markdown解析及代码高亮

在后台支持markdown编辑后,前台页面的博客文章页面也得要支持对markdown得解析。

1、安装依赖

在Django项目中使用markdown2库实现代码高亮,安装markdown2和pygments这两个Python库。markdown2用于解析Markdown文本,而pygments用于代码高亮。

1
pip install markdown2 pygments

2、配置Markdown解析器

在Django视图中,需要导入markdown2模块,并使用它来解析Markdown文本。同时,要启用fenced-code-blocks扩展,以便markdown2能正确识别代码块。

1
import markdown2

markdown2的扩展说明见 https://github.com/trentm/python-markdown2/wiki/Extras
在这里加入了

1
2
extras=["code-color", "fenced-code-blocks", "cuddled-lists", "tables",
"with-toc", "code-friendly"]

视图实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 详情页视图实现.
def post_detail(request, id):
try:
post_obj = BlogPost.objects.get(id=id)
html_content = markdown2.markdown(post_obj.content,
extras=["code-color", "fenced-code-blocks", "cuddled-lists", "tables",
"with-toc", "code-friendly"])
html_content = html_content.replace('<table>', '<table class="table table-bordered">')
html_content = html_content.replace('<img src=', '<img style="max-width:100%;height:auto;" src=')
context = {"post_obj": post_obj,
"html_content": html_content,
"hot_posts": get_hot_posts(),
"tags": get_all_tags(),
"post_grouped_by_year": get_post_groped_by_year(),
'categories': get_categories(),
'social_infos': get_socialinfo()}
except BlogPost.DoesNotExist:
raise Http404("Post does not exist")
return render(request, "blog/post.html", context)

3、在模板中显示HTML

在Django模板中,直接输出转换后的HTML内容。使用|safe过滤器告诉模板引擎不要转义HTML代码。

1
2
3
<div class="lyear-arc-detail">
{{ html_content|safe }}
</div>

4、导出高亮的css文件并引入css

有了上面的步骤,只是可以解析了markdown成html并显示,最终代码的高亮是通过css 来控制显示的,执行以下命令将高亮的css文件导出。

1
pygmentize -S default -f html -a .codehilite > markdown_highlighy.css

pyments的官方文档 https://pygments.org/ 查看一共有多少种风格,可以参考网址 https://pygments.org/docs/styles/#getting-a-list-of-available-styles
将生成的markdown_highlighy.css文件拷入到static/blog/css下
在模板页面中引入css

1
<link rel="stylesheet" type="text/css" href="{% static 'blog/css/markdown_highlighy.css' %}" />

5、查看效果

可以看到markdown可以正常解析,代码也可以高亮显示了。

在这里插入图片描述

三、全套代码

https://gitee.com/xiejava/ishareblog


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注:微信公众号,一起学习成长!

一个人活成一个团队:python的django项目devops实战

发表于 2024-08-11 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 1.5k | 阅读时长 ≈ 5

对于开发团队来说提高软件交付的速度和质量是一个永恒的话题,对于个人开发者来说同样如此。作为一个码农,一定会有几个自己私有的小项目,从需求管理到开发到测试到部署运维都得要自己来,将自己一个人活成一个团队。

DevOps(Development和Operations的组合),旨在通过自动化、协作和共享责任来提高软件开发和运维的效率、质量和安全性。作为一个人的团队,也可通过devops实践来提高对自己项目的效率和质量,使产品持续开发、持续集成、持续测试、持续部署、持续监控,非常频繁地发布新版本。本文就以一个实际的python的django项目来运用阿里的云效devops平台来进行实战。
在这里插入图片描述
DevOps平台工具有很多,最常见的就是大名顶顶的Jenkins,作为个人开发者要准备相应的硬件资源,还要要自己维护一套Jenkins有点麻烦。这里直接就选择成熟的阿里云效devops https://devops.aliyun.com/ ,这套平台基础版是免费的,对于个人开发者来说已经够用了。

一、需求规划

个人项目虽小,但是也得要有相应的规划,至少得有个需求清单来进行需求的规划和跟踪,哪些需求已经完成了,哪些还需要进行开发做到自己心中有数。
可以在云效的项目协作中创建一个项目进行管理。
在这里插入图片描述

在这里我创建了一个xiejava的博客项目
在这里插入图片描述

在这里我们就可以将自己规划的需求录入进来做好自己的需求跟踪清单
在这里插入图片描述

可以规划自己的版本,将需求跟踪清单里的需求纳入到版本迭代计划。
在这里插入图片描述

在迭代计划中可以看到这个迭代要完成的需求清单。
在这里插入图片描述

二、代码管理

即使是最简单的项目,建议还是通过代码仓库进行代码的版本管理,我的代码是放到码云https://gitee.com/xiejava/ishareblog 进行托管的,也可以托管到云效自己的代码管理仓库。
有了代码仓库,可以通过在云效构建流水线来进行自动构建、自动测试、自动部署了。

三、创建流水线

在云效中创建ishareblog的自动发布流水线,整个流水线包括获取代码、测试、构建、部署。
在这里插入图片描述

1、配置流水线源

流水线源可以配置云效自己的代码库,也可以配置其他的代码库,如我里是配置的码云代码库。
可以开启代码源触发,开启后一旦代码库有提交操作,就会自动触发流水线工作。
在这里插入图片描述

需要说明的是,如果是外部的代码仓库,需要在外部的代码仓库中添加Webhook触发设置

如我的是码云的仓库,就要在码云的仓库中添加Webhook的配置
在这里插入图片描述

四、自动测试

在测试环节,配置了python代码扫描和Python单元测试。
python代码扫描用的是云效默认的配置
比较麻烦的是Python单元测试,Python单元测试需要在Python项目中写测试用例,还要配置测试命令。
在Python项目中写测试用例见《django集成pytest进行自动化单元测试实战》。
配置测试命令就是在测试服务其中进行发布测试的所有shell命令
在这里插入图片描述

配置测试命令就是在测试服务其中进行发布测试的所有shell命令
作为一个django的项目测试命令参考如下:

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
# pytest default command
# 安装mysql客户端
sudo apt-get update
sudo apt-get install -y libmysqlclient-dev

# 安装新版本的SQLite3
# wget https://www.sqlite.org/2024/sqlite-autoconf-3460000.tar.gz

# 安装依赖
sudo pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
sudo pip install pysqlite3 -i https://pypi.tuna.tsinghua.edu.cn/simple
sudo pip install pysqlite3-binary -i https://pypi.tuna.tsinghua.edu.cn/simple

# 替换Django的sqlite3的驱动文件
sudo cp -f /root/workspace/ishareblog_J18t/change_set/base.py /usr/local/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py

# 初始化数据库
sudo python manage.py makemigrations --settings=ishareblog.settings_test
sudo python manage.py migrate --settings=ishareblog.settings_test

# 启动django服务
sudo nohup python manage.py runserver 8000 --settings=ishareblog.settings_test &

PORT=8000 # 替换为您想要检查的端口号
NEXT_COMMAND="sudo pytest --html=report/index.html" # 通过pytest进行单元测试

until nc -z localhost $PORT; do
echo "Port $PORT is not ready - waiting..."
curl http://localhost:8000
sleep 1
done

echo "Port $PORT is ready"
eval "$NEXT_COMMAND"

ps aux | grep python


# 通过pytest进行单元测试
#sudo pytest --html=report/index.html

pkill -f manage.py

ps aux | grep python

因为在单元测试中还做了接口测试,这里会要启动djang服务,进行接口测试,测试完成后还要停止服务。
可以在流水线执行完后查看扫描报告和测试报告
在这里插入图片描述

扫描报告
代码扫描报告,报出来的大部分是格式规范的问题。
在这里插入图片描述

测试报告
自动化测试报告是通过pytest测试完成形成的报告。
在这里插入图片描述

五、自动构建

自动构建将会将构建好的制品打包上传至构建服务器上。
在这里插入图片描述

六、自动部署

在这里插入图片描述

也可以配置部署后的通知邮件,比如部署成功或失败后发邮件通知。

在这里插入图片描述

最后通过统计报表可以看到流水线近段期间的执行情况

在这里插入图片描述

七、总结

DevOps通过自动化的流程,使得构建、测试、发布软件能够更加地快捷、频繁和可靠。本文通过一个python的django个人博客应用进行了DevOps的实战,通过DevOps拉通开发和运维,通过应用云效的DevOps平台实现自动化“软件交付”的流程,使得构建、测试、发布软件能够更加地快捷、频繁和可靠,提交研发交付效率。作为个人项目也是可以应用devops提高效率。


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注:微信公众号,一起学习成长!

django快速实现个人博客(附源码)

发表于 2024-08-10 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 3.7k | 阅读时长 ≈ 19

Django作为一款成熟的Python Web开发框架提供了丰富的内置功能,如ORM(对象关系映射)、Admin管理界面、URL分发、模板系统、表单处理等,使得开发者能够快速搭建Web应用,大幅提高了开发效率。以前写过一篇博文《Django+Vue快速实现博客网站》介绍了通过Djang+Vue快速实现博客网站,django+vue作为个人博客来说稍显复杂,部署起来也比较麻烦,Vue的单页面架构也不利于SEO,更简单的解决方案其实还是用django的模板系统快速构建web应用,对于个人博客来说部署和运维更加简单也利于SEO。下面介绍如何快速的通过django模板系统快速实现个人博客。

一、工程目录组织结构

在这里插入图片描述

二、模型及管理实现

模型及管理端的实现沿用《Django+Vue快速实现博客网站》文章中的实现,用Django搭建很快很简单。
模型很简单,根据博客要显示的内容包括有‘文章分类’、‘文章标签’、‘博客文章’、‘站点信息’、‘社交信息’、‘聚焦’,模型定义分别如下: 这里要说明的是因为博客文章内容准备用markdown编写,所以引入了mdeditor from mdeditor.fields import MDTextField 内容字段content=MDTextField(verbose_name='内容')
模型代码示例如下:

1、模型

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
from django.db import models
from common.basemodel import BaseModel
from mdeditor.fields import MDTextField
# Create your models here.
'''文章分类'''
class BlogCategory(BaseModel):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=50,verbose_name='分类名称',default='')
href = models.CharField(max_length=100,verbose_name='分类路径',default='')

def __str__(self):
return self.title

class Meta:
verbose_name = '文章分类'
verbose_name_plural = '文章分类'


'''文章标签'''


class Tag(BaseModel):
id=models.AutoField(primary_key=True)
tag=models.CharField(max_length=20, verbose_name='标签')

def __str__(self):
return self.tag

class Meta:
verbose_name='标签'
verbose_name_plural='标签'


'''博客文章'''
class BlogPost(BaseModel):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=200, verbose_name='文章标题', unique = True)
category = models.ForeignKey(BlogCategory, blank=True,null=True, verbose_name='文章分类', on_delete=models.DO_NOTHING)
isTop = models.BooleanField(default=False, verbose_name='是否置顶')
isHot = models.BooleanField(default=False, verbose_name='是否热门')
isShow = models.BooleanField(default=False, verbose_name='是否显示')
summary = models.TextField(max_length=500, verbose_name='内容摘要', default='')
content = MDTextField(verbose_name='内容')
viewsCount = models.IntegerField(default=0, verbose_name="查看数")
commentsCount = models.IntegerField(default=0, verbose_name="评论数")
tags = models.ManyToManyField(to=Tag, related_name="tag_post", blank=True, default=None, verbose_name="标签")
blogSource = models.CharField(max_length=200, blank=True, null=True, default='',verbose_name='文章来源')
pubTime = models.DateTimeField(blank=True, null=True, verbose_name='发布日期')

@property
def tag_list(self):
return ','.join([i.tag for i in self.tags.all()])

def __str__(self):
return self.title

class Meta:
verbose_name = '博客文章'
verbose_name_plural = '博客文章'


'''站点信息'''


class Site(BaseModel):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=50, verbose_name='站点名称', unique = True)
avatar = models.CharField(max_length=200, verbose_name='站点图标')
slogan = models.CharField(max_length=200, verbose_name='站点标语')
domain = models.CharField(max_length=200, verbose_name='站点域名')
notice = models.CharField(max_length=200, verbose_name='站点备注')
desc = models.CharField(max_length=200, verbose_name='站点描述')

def __str__(self):
return self.name

class Meta:
verbose_name = '站点信息'
verbose_name_plural = '站点信息'


'''社交信息'''


class Social(BaseModel):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=20, verbose_name='标题')
icon = models.CharField(max_length=200, verbose_name='图标')
color = models.CharField(max_length=20, verbose_name='颜色')
href = models.CharField(max_length=100, verbose_name='路径')

def __str__(self):
return self.title

class Meta:
verbose_name = '社交信息'
verbose_name_plural = '社交信息'


'''聚焦'''


class Focus(BaseModel):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=20, verbose_name='标题')
img = models.CharField(max_length=100, verbose_name='路径')

def __str__(self):
return self.title

class Meta:
verbose_name = '聚焦'
verbose_name_plural = '聚焦'


'''友链'''


class Friend(BaseModel):
id = models.AutoField(primary_key=True)
siteName = models.CharField(max_length=20, verbose_name='友链站点名称')
path = models.CharField(max_length=100, verbose_name='地址路径')
desc = models.CharField(max_length=200, verbose_name='描述')

def __str__(self):
return self.siteName

class Meta:
verbose_name = '友链'
verbose_name_plural = '友链'

2、admin管理

实际上只要把模型注册到admin就可以了

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
from django.contrib import admin
from blog.models import *
# Register your models here.
@admin.register(BlogCategory)
class BlogCategoryAdmin(admin.ModelAdmin):
admin.site.site_title="ishareblog后台"
admin.site.site_header="ishareblog后台"
admin.site.index_title="ishareblog管理"

list_display = ['id', 'title', 'href']

@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
list_display = ['title','category','isTop','isHot']
search_fields = ('title',)

@admin.register(Site)
class SiteAdmin(admin.ModelAdmin):
list_display = ['name','slogan','domain','desc']

@admin.register(Social)
class SocialAdmin(admin.ModelAdmin):
list_display = ['title','href']

@admin.register(Focus)
class FoucusAdmin(admin.ModelAdmin):
list_display = ['title','img']

@admin.register(Friend)
class FoucusAdmin(admin.ModelAdmin):
list_display = ['siteName','path','desc']

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['id','tag']

三、博客展现实现

博客前端展现用django的模板技术实现。在网上找了一个基于Bootstrap v4.3.1的小清新风格HTML博客模板,https://gitee.com/yinqi/Light-Year-Blog 这个博客模只有三个页面,首页,详细页和About页面,样式和js都不多,比较简单。将html模板放入到templates的blog目录,为了便于维护将一些公共部分抽到了base.html,index.html和post.html 通过{ % extends 'blog/base.html' % }进行应用

1、视图实现

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from django.http import HttpResponse, Http404
from django.template import loader
from django.core.paginator import Paginator
from blog.models import BlogPost, Tag, BlogCategory
from django.shortcuts import render
from django.db.models import Count
from django.db.models.functions import TruncYear

import markdown2


# 首页/列表页视图实现.
def index(request):
category_id = request.GET.get('category')
tag_id = int(request.GET.get('tag',0))
year = request.GET.get('year')
search = request.GET.get('search')
if category_id:
blogpost_list = BlogPost.objects.filter(category=category_id, isShow=True).order_by('-isTop', '-pubTime')
elif tag_id:
blogpost_list = BlogPost.objects.filter(tags__id=tag_id, isShow=True).order_by('-isTop', '-pubTime')
elif year:
blogpost_list = BlogPost.objects.filter(pubTime__year=year, isShow=True).order_by('-isTop', '-pubTime')
elif search:
blogpost_list = BlogPost.objects.filter(content__icontains=search, isShow=True).order_by('-isTop', '-pubTime')
else:
# 筛选出需要显示的博客文章
blogpost_list = BlogPost.objects.filter(isShow=True).order_by('-isTop', '-pubTime', '-update_time')
# 每页显示的数量
per_page = 10

# 创建分页器实例
paginator = Paginator(blogpost_list, per_page)

# 获取当前页码,如果没有提供,则默认为第一页
page_number = request.GET.get('page') or 1

# 获取当前页的数据
page_obj = paginator.get_page(page_number)

# 计算显示的页码范围
current_page = int(page_number)
pages_to_show = 11 # 当前页前后各5页加上当前页共11页
start_page = max(current_page - 5, 1)
end_page = min(start_page + pages_to_show - 1, paginator.num_pages)

template = loader.get_template("blog/index.html")
context = {
"page_obj": page_obj,
'start_page': start_page,
'end_page': end_page,
'hot_posts': get_hot_posts(),
'tags': get_all_tags(),
'post_grouped_by_year':get_post_groped_by_year(),
'categories': get_categories(),
'category_id': category_id,
'tag_id': tag_id,
'year': year,
'search': search,
}
return HttpResponse(template.render(context, request))


# 详情页视图实现.
def post_detail(request, id):
try:
post_obj = BlogPost.objects.get(id=id)
html_content = markdown2.markdown(post_obj.content,
extras=["code-color", "fenced-code-blocks", "cuddled-lists", "tables",
"with-toc", "highlightjs-lang"])
html_content = html_content.replace('< table >', '< table class="table table-bordered" >')
html_content = html_content.replace('< img src=', '< img style="max-width:100%;height:auto;" src= ')
context = {"post_obj": post_obj, "html_content": html_content, "hot_posts": get_hot_posts(),"tags": get_all_tags(),"post_grouped_by_year":get_post_groped_by_year(),'categories': get_categories()}
except BlogPost.DoesNotExist:
raise Http404("Post does not exist")
return render(request, "blog/post.html", context)


def get_hot_posts():
# 获取点赞数最高的前5篇文章
hot_posts = BlogPost.objects.filter(isShow=True).order_by('-viewsCount', '-pubTime')[:5]
return hot_posts


def get_all_tags():
# 获取所有的标签
tags = Tag.objects.all() # 获取所有的标签
return tags

def get_post_groped_by_year():
# 将发布日期截断为年份,并计算每年的文章数量。
post_grouped_by_year = (
BlogPost.objects
.annotate(year=TruncYear('pubTime'))
.values('year') # 返回的字典包含'year'键
.annotate(publication_count=Count('id')) # 计算每年的文章数量
.order_by('-year') # 按年排序
)
return post_grouped_by_year

def get_categories():
# 获取所有分类
categories = BlogCategory.objects.all()
return categories

2、模板实现

静态文件如css、js等放到static的blog目录,html模板文件放到templates的blog目录
在setting.py文件中配置 STATIC_URL = 'static/',在html模板文件中通过{ % load static % } 将静态文件的地址引用进来
将公共部分抽取出来形成base.html

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
{ % load static % }
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>XieJava的博客</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="" />
<meta name="keywords" content="" />
<meta name="author" content="xiejava" />
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
<link rel="stylesheet" type="text/css" href="{ % static 'blog/css/bootstrap.min.css' % }" />
<link rel="stylesheet" type="text/css" href="{ % static 'blog/css/materialdesignicons.min.css' % }" />
<link rel="stylesheet" type="text/css" href="{ % static 'blog/css/style.min.css' % }" />
</head>
<body>
<header class="lyear-header text-center" style="background-image:url(images/left-bg.jpg);">
<div class="lyear-header-container">
<div class="lyear-mask"></div>
<h1 class="lyear-blogger pt-lg-4 mb-0"><a href="{ % url 'index' % }">XieJava的博客</a></h1>
<nav class="navbar navbar-expand-lg">
<a class="navbar-toggler" data-toggle="collapse" data-target="#navigation" aria-controls="navigation" aria-expanded="false" aria-label="Toggle navigation">
<div class="lyear-hamburger">
<div class="hamburger-inner"></div>
</div>
</a>

<div id="navigation" class="collapse navbar-collapse flex-column">
<div class="profile-section pt-3 pt-lg-0">
<img class="profile-image mb-3 rounded-circle mx-auto" src="https://img9.doubanio.com/icon/ul70489051-4.jpg" width="120" height="120" alt="xiejava" >
<div class="lyear-sentence mb-3">
记录最好的自己<br>
写是为了更好的思考,坚持写作,力争更好的思考。
</div>
<hr>
</div>

<ul class="navbar-nav flex-column text-center">
<li class="nav-item active">
<a class="nav-link" href="{ % url 'index' % }">首页</a>
</li>
{ % for category in categories % }
<li class="nav-item">
<a class="nav-link" href="{ % url 'index' % }?category={ { category.id } }">{ { category.title } }</a>
</li>
{ % endfor % }
<li class="nav-item">
<a class="nav-link" href="{ % url 'index' % }">关于我</a>
</li>
</ul>

<div class="my-2 my-md-3">
<form class="lyear-search-form form-inline justify-content-center pt-3">
<input type="text" id="search" name="search" class="form-control mr-md-1" placeholder="搜索关键词" />
</form>
</div>
</div>
</nav>
</div>
</header>
<div class="lyear-wrapper">
<section class="mt-5 pb-5">
<div class="container">

<div class="row">
<!-- 文章列表 -->
<div class="col-xl-8">
<!-- 内容 -->
{ % block content % }
<!-- 默认内容 -->
{ % endblock % }
</div>
<!-- 内容 end -->

<!-- 侧边栏 -->
<div class="col-xl-4">
<div class="lyear-sidebar">
<!-- 热门文章 -->
<aside class="widget widget-hot-posts">
<div class="widget-title">热门文章</div>
<ul>
{ % for post in hot_posts % }
<li>
<a href="{ % url 'post_detail' id=post.id % }">{ { post.title } }</a> <span>{ { post.pubTime } }</span>
</li>
{ % endfor % }
</ul>
</aside>

<!-- 归档 -->
<aside class="widget">
<div class="widget-title">归档</div>
<ul>
{ % for post in post_grouped_by_year % }
<li><a href="{ % url 'index' % }?year={ { post.year|date:'Y' } }" >{ % if year == post.year|date:'Y' % }<b>{ { post.year|date:'Y' } } 年 </b>{ % else % }{ { post.year|date:'Y' } } 年{ % endif % }</a> ({ { post.publication_count } })</li>
{ % endfor % }
</ul>
</aside>

<!-- 标签 -->
<aside class="widget widget-tag-cloud">
<div class="widget-title">标签 </div>
<div class="tag-cloud">
{ % for tag in tags % }
<a href="{ % url 'index' % }?tag={ { tag.id } }" { % if tag_id == tag.id % }class="badge badge-primary"{ % else % }class="badge badge-light"{ % endif % }>{ { tag.tag } }</a>
{ % endfor % }
</div>
</aside>
</div>
</div>
<!-- 侧边栏 end -->
</div>

</div>
<!-- end container -->
</section>
</div>
<script type="text/javascript" src="{ % static 'blog/js/jquery.min.js' % }"></script>
<script type="text/javascript" src="{ % static 'blog/js/jquery.nicescroll.min.js' % }"></script>
<script type="text/javascript" src="{ % static 'blog/js/bootstrap.min.js' % }"></script>
<script type="text/javascript" src="{ % static 'blog/js/main.min.js' % }"></script>
</body>
</html>

博客首页/列表页
通过{ % extends 'blog/base.html' % } 将公共部门引入进来后index.html的内容就简洁了很多
index.html

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

{ % extends 'blog/base.html' % }

<!-- 内容 -->
{ % block content % }
{ % if page_obj.object_list.count > 0 % }
{ % for blogpost in page_obj.object_list % }
<article class="lyear-arc">
<div class="arc-header">
<h2 class="arc-title"><a href="article/{ { blogpost.id } }">{ { blogpost.title } }</a></h2>
<ul class="arc-meta">
<li><i class="mdi mdi-calendar"></i> { { blogpost.pubTime } }</li>
<li><i class="mdi mdi-tag-text-outline">
</i> { % for tag in blogpost.tags.all % }<a href="{ % url 'index' % }?tag={ { tag.id } }">{ { tag.tag } }</a>&nbsp{ % endfor % }</li>
<!--<li><i class="mdi mdi-comment-multiple-outline"></i> <a href="#">3 评论</a></li>-->
<li><i class="mdi mdi-heart-outline"></i> <a href="#">{ { blogpost.viewsCount } } 喜欢</a></li>
</ul>
</div>

<div class="arc-synopsis">
<p>{ { blogpost.summary } }</p>
</div>
</article>
{ % endfor % }


<!-- 分页 -->
<div class="row">
<div class="col-lg-12">
<ul class="pagination">
{ % if page_obj.has_previous % }
<li class="page-item"><a class="page-link" href="?page={ { page_obj.previous_page_number } }"><i class="mdi mdi-chevron-left"></i></a></li>
{ % endif % }

{ % for page_no in page_obj.paginator.page_range % }
{ % if page_no >= start_page and page_no <= end_page % }
{ % if page_no == page_obj.number % }
<li class="page-item active"><a class="page-link" href="#">{ { page_no } }</a></li>
{ % else % }
<li class="page-item"><a class="page-link" href="?page={ { page_no } }{ % if tag_id % }&tag={ { tag_id } }{ % endif % }{ % if year % }&year={ { year } }{ % endif % }{ % if search % }&search={ { search } }{ % endif % }">{ { page_no } }</a></li>
{ % endif % }
{ % endif % }
{ % endfor % }

{ % if page_obj.has_next % }
<li class="page-item"><a class="page-link" href="?page={ { page_obj.next_page_number } }{ % if tag_id % }&tag={ { tag_id } }{ % endif % }{ % if year % }&year={ { year } }{ % endif % }{ % if search % }&search={ { search } }{ % endif % }"><i class="mdi mdi-chevron-right"></i></a></li>
{ % endif % }

<p>总页数: { { page_obj.paginator.num_pages } }</p>
</ul>
</div>
</div>
{ % else % }
<p> 没有找到文章 </p>
{ % endif % }
<!-- 分页 end -->
{ % endblock % }
<!-- 内容 end -->

博客详情页post.html

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
{ % extends 'blog/base.html' % }
<!-- 文章阅读 -->

{ % block content % }

<article class="lyear-arc">
<div class="arc-header">
<h2 class="arc-title"><a href="#">{ { post_obj.title } }</a></h2>
<ul class="arc-meta">
<li><i class="mdi mdi-calendar"></i> { { post_obj.pubTime } }</li>
<li> { % for tag in post_obj.tags.all % }<a href="{ % url 'index' % }?tag={ { tag.id } }">{ { tag.tag } }</a>{ % endfor % }</li>
<!--<li><i class="mdi mdi-comment-multiple-outline"></i> <a href="#">3 评论</a></li>-->
<li><i class="mdi mdi-heart-outline"></i> <a href="#">{ { post_obj.viewsCount } } 喜欢</a></li>
</ul>
</div>

<div class="arc-preview">
<img src="images/blog/post-1.png" alt="" class="img-fluid rounded" />
</div>

<div class="lyear-arc-detail">
{ { html_content|safe } }
</div>

</article>

{ % endblock % }
<!-- 内容 end -->

四、部署及效果

在部署之前执行python manage.py collectstatic 将admin等其他模块用到的静态文件统一输出到static的目录。
通过 python manage.py runserver 启动应用就可以看到效果。
实际效果见 http://iblog.ishareread.com/
博客首页
在这里插入图片描述

博客详情页
在这里插入图片描述

五、源代码

所有源代码及说明见 https://gitee.com/xiejava/ishareblog


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注:微信公众号,一起学习成长!

django集成pytest进行自动化单元测试实战

发表于 2024-08-03 | 更新于: 2025-07-11 | 分类于 技术 | | 阅读次数:
字数统计: 1.3k | 阅读时长 ≈ 5

在Django项目中集成Pytest进行单元测试可以提高测试的灵活性和效率,相比于Django自带的测试框架,Pytest提供了更为丰富和强大的测试功能。本文通过一个实际项目ishareblog介绍django集成pytest进行自动化单元测试实战。

一、引入pytest相关的包

1
2
3
pip install pytest
pip install pytest-django
pip install pytest-html

其中pytest-django插件,它提供了Django和Pytest之间的桥梁,pytest-html 是一个 pytest 的插件,用于生成详细的 HTML 测试报告。这个插件能够将 pytest 运行的结果转化为一个直观、易于阅读的 HTML 格式报告,这对于分享测试结果、审查测试覆盖率以及归档测试历史非常有帮助。

二、配置pytest

1、将django的配置区分测试环境、开发环境和生产环境

因为测试环境、开发环境和生产环境的环境配置参数不一样,一个好的实践是将开发、测试和生产环境通过配置区分开,django的配置主要集中在项目的settings.py文件,这里通过settings.py的配置文件将开发、测试、生产区分开,不同的环境调用不通的配置文件。

在这里插入图片描述

因为大部分的配置参数都是一样的,在这里我将公共的配置参数都抽到了base.py,环境配置中有差异的部分分别放到各自的配置文件中,如开发环境用的是mysql,测试环境用sqlite3,就可以将不同的配置给区分开。
测试环境是settings_test.py,这里除了数据库的配置不一样,其他都沿用基础的公共配置。settings_test.py配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from .base import *

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'test_db.sqlite3'),
}
}

2、配置pytest

在Django项目根目录下,创建或编辑pytest.ini文件,来配置Pytest。
在这里插入图片描述

pytest.ini代码如下:

1
2
3
4
[pytest]
DJANGO_SETTINGS_MODULE = ishareblog.settings_test

python_files = tests.py test_*.py *_tests.py

DJANGO_SETTINGS_MODULE = ishareblog.settings_test 指定了pytest用到的环境配置
python_files = tests.py test_*.py *_tests.py 指定了pytest将测试以test开头的py文件中的测试用例。

三、编写测试用例

接下来,可以在tests.py或test_*.py文件中编写你的测试用例。由于pytest-django插件的存在,你可以像平常一样使用Django的测试机制,同时也能享受Pytest带来的便利。以下以我的ishareblog博客代码通过业务测试和接口测试来编写测试用例。

在这里插入图片描述

1、业务测试

我的isharebog业务相对简单,主要是测试验证业务模型模块的增删改查是否符合预期。
业务测试tests.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
import pytest
from django.test import TestCase
from blog.models import BlogCategory

@pytest.mark.django_db
class TestBlogCategory(TestCase):

def setUp(self):
self.blogcategory = BlogCategory.objects.create(id=1,title="Test Category", href='/category/1')

def test_BogCategoryModel(self):
blog_category = BlogCategory.objects.get(id=self.blogcategory.id)
self.assertEqual(blog_category.title, "Test Category")
self.assertEqual(blog_category.href, '/category/1')


@pytest.mark.django_db
def test_blog_category_create():
blogcategory = BlogCategory.objects.create(id=1,title="Test Category", href='/category/1')
category_count = BlogCategory.objects.count()
assert category_count > 0, "Blog category was not created category_count=0."
assert blogcategory.id > 0, "Blog category was not created."
assert blogcategory.title == "Test Category", "Blog category title is wrong."
assert blogcategory.href == "/category/1", "Blog category href is wrong."


@pytest.mark.django_db
def test_blog_category_query():
category_count = len(BlogCategory.objects.all())
assert category_count >= 0, "Blog category query error."


if __name__ == '__main__':
pytest.main(["-s", "-v", "-p", "no:warnings", "--tb=short", "--html=report.html", "blog/tests.py"])

业务测试举了通过测试类和测试方法写的测试用例,分别对博客目录进行添加和查询编写了测试用例。

2、接口测试

接口是暴露给前端程序调用的,接口测试主要是测试接口正不正常,接口值是不是符合预期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import pytest

host = "http://localhost:8000"


class TestApi:
def test_getcategory_list(self):
url = f'{host}/api/category/'
response = requests.get(url)
assert response.status_code == 200, f'Expected status code 200 but got {response.status_code}'
assert response.json() != None, f'Expected to get json response but got {response.text}'
print(response.json())

def test_getpost_list(self):
url = f'{host}/api/post/list'
response = requests.get(url)
assert response.status_code == 200, f'Expected status code 200 but got {response.status_code}'
assert response.json() != None, f'Expected to get json response but got {response.text}'


if __name__ == '__main__':
pytest.main(["-s", "-v", "-p", "no:warnings", "--tb=short", "--html=report.html", "api/tests.py"])

接口测试部分,对获取目录的API接口和文章列表的API接口编写了测试用例。

四、进行测试

最后可以分别在blog目录和api目录下运行test.py 分别进行业务和接口的单元测试。
注意在进行测试之前需要执行 python manage.py makemigrations --settings=ishareblog.settings_test 初始化环境。
在进行api接口测试之前需要将django的应用服务启动 python manage.py runserver 8000 --settings=ishareblog.settings_test 启动的时候也带上测试环境的配置。
可以通过pytest --html=report.html 自动执行所有的单元测试,并生成可读的html的测试报告。
在这里插入图片描述

pytest生成的report.html测试报告
在这里插入图片描述

以上通过一个ishareblog的实际项目介绍django集成pytest进行自动化单元测试实战。
ishareblog的所有代码包括pytest的配置见 https://gitee.com/xiejava/ishareblog


博客地址:http://xiejava.ishareread.com/


“fullbug”微信公众号

关注:微信公众号,一起学习成长!

<123…21>
XieJava

XieJava

203 日志
11 分类
26 标签
RSS
GitHub
友情链接
  • 爱分享读书
  • CSDN
  • 豆瓣
© 2025 XieJava | Site words total count: 419.2k

主题 — NexT.Muse
0%