• UnitTesting 单元测试


    1. 测试分为两种及详细介绍测试书籍:

      1.1 Unit Test : 单元测试

      - test the business logic in your app : 测试应用中的业务逻辑

      1.2 UI  Test :  界面测试

      - test the UI of your app : 测试应用中的界面

      1.3 测试书籍网址:

    《Testing Swift》icon-default.png?t=N7T8https://www.hackingwithswift.com/store/testing-swift

    2. ViewModel 单元测试

      2.1 创建 ViewModel,UnitTestingBootcampViewModel.swift

    1. import Foundation
    2. import SwiftUI
    3. import Combine
    4. /// 单元测试 ViewModel
    5. class UnitTestingBootcampViewModel: ObservableObject{
    6. @Published var isPremium: Bool
    7. @Published var dataArray: [String] = []
    8. @Published var selectedItem: String? = nil
    9. let dataService: NewDataServiceProtocol
    10. var cancellable = Set<AnyCancellable>()
    11. init(isPremium: Bool, dataService: NewDataServiceProtocol = NewMockDataService(items: nil)) {
    12. self.isPremium = isPremium
    13. self.dataService = dataService
    14. }
    15. /// 添加子项
    16. func addItem(item: String){
    17. // 为空不往下执行
    18. guard !item.isEmpty else { return }
    19. self.dataArray.append(item)
    20. }
    21. /// 选中项
    22. func selectItem(item: String){
    23. if let x = dataArray.first(where: {$0 == item}){
    24. selectedItem = x
    25. }else{
    26. selectedItem = nil
    27. }
    28. }
    29. /// 保存项
    30. func saveItem(item: String) throws{
    31. guard !item.isEmpty else{
    32. throw DataError.noData
    33. }
    34. if let x = dataArray.first(where: {$0 == item}){
    35. print("Save item here!!! \(x)")
    36. } else {
    37. throw DataError.itemNotFound
    38. }
    39. }
    40. /// 错误信息
    41. enum DataError: LocalizedError{
    42. case noData
    43. case itemNotFound
    44. }
    45. /// 请求返回数据
    46. func downloadWithEscaping() {
    47. dataService.downloadItemsWithEscaping { [weak self] returnedItems in
    48. self?.dataArray = returnedItems
    49. }
    50. }
    51. /// 下载用到的组合
    52. func downloadWithCombine() {
    53. dataService.downloadItemsWithCombine()
    54. .sink { _ in
    55. } receiveValue: { [weak self] returnedItems in
    56. self?.dataArray = returnedItems
    57. }
    58. .store(in: &cancellable)
    59. }
    60. }

      2.2 创建测试文件

        当创建项目时,没有选择 Include Tests/包含测试 选项时,需要添加文件去对应项目,不然测试文件会报 No such module 'XCTest' 编译错误

        添加单元测试文件:

        方法一 : 选择项目 -> 菜单栏 Editor -> Add Target... -> 弹出对话框,选择 Test 栏下 -> Unit Testing Bundle -> 填写信息/可默认 -> Finish,完成创建单元测试文件。

        方法二 : 选择项目,点击 PROJECT 列,最下的 + 按钮,弹出对话框,选择 Test 栏下 ,后面步骤与上一致

        创建单元测试文件 UnitTestingBootcampViewModel_Tests.swift

    1. import XCTest
    2. import Combine
    3. /// 导入项目
    4. @testable import SwiftfulThinkingAdvancedLearning
    5. // 《Testing Swift》 测试书籍
    6. // 书籍网址: https://www.hackingwithswift.com/store/testing-swift
    7. // Naming Structure: test_UnitOfWork_StateUnderTest_ExpectedBehavior - 结构体命名: 测试_工作单元_测试状态_预期的行为
    8. // Naming Structure: test_[struct or class]_[variable or function]_[expected result] - 测试_[结构体 或者 类的名称]_[类中的变量名 或者 函数名称]_[预期结果 预期值]
    9. // Testing Structure: Given, When, Then - 测试结构: 给定,什么时候,然后
    10. final class UnitTestingBootcampViewModel_Tests: XCTestCase {
    11. /// 解决多次引用相同的类
    12. var viewModel: UnitTestingBootcampViewModel?
    13. var cancellables = Set<AnyCancellable>()
    14. /// 开始设置数据
    15. override func setUpWithError() throws {
    16. // Put setup code here. This method is called before the invocation of each test method in the class.
    17. viewModel = UnitTestingBootcampViewModel(isPremium: Bool.random())
    18. }
    19. /// 结束重置数据
    20. override func tearDownWithError() throws {
    21. // Put teardown code here. This method is called after the invocation of each test method in the class.
    22. viewModel = nil
    23. cancellables.removeAll()
    24. }
    25. /// 单元测试函数名,根据命名规则命名:测试_类名称_是否高质量_应该为真
    26. func test_UnitTestingBootcampViewModel_isPremium_shouldBeTrue(){
    27. // Given
    28. let userIsPremium: Bool = true
    29. // When
    30. let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
    31. // Then
    32. XCTAssertTrue(vm.isPremium)
    33. }
    34. /// 单元测试函数名 根据命名规则命名:测试_类名称_是否高质量_应该为假
    35. func test_UnitTestingBootcampViewModel_isPremium_shouldBeFalse(){
    36. // Given
    37. let userIsPremium: Bool = false
    38. // When
    39. let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
    40. // Then
    41. XCTAssertFalse(vm.isPremium)
    42. }
    43. /// 单元测试函数名 根据命名规则命名:测试_类名称_是否高品质_注入值
    44. func test_UnitTestingBootcampViewModel_isPremium_shouldBeInjectedValue(){
    45. // Given
    46. let userIsPremium: Bool = Bool.random()
    47. // When
    48. let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
    49. // Then
    50. XCTAssertEqual(vm.isPremium, userIsPremium)
    51. }
    52. /// 单元测试函数名 根据命名规则命名 - 注入值_压力 / for 循环
    53. func test_UnitTestingBootcampViewModel_isPremium_shouldBeInjectedValue_stress(){
    54. for _ in 0 ..< 10 {
    55. // Given
    56. let userIsPremium: Bool = Bool.random()
    57. // When
    58. let vm = UnitTestingBootcampViewModel(isPremium: userIsPremium)
    59. // Then
    60. XCTAssertEqual(vm.isPremium, userIsPremium)
    61. }
    62. }
    63. /// 单元测试函数名 根据命名规则命名 - 数组_预期值:为空
    64. func test_UnitTestingBootcampViewModel_dataArray_shouldBeEmpty(){
    65. // Given
    66. // When
    67. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    68. // Then 断言 = 判定
    69. XCTAssertTrue(vm.dataArray.isEmpty)
    70. XCTAssertEqual(vm.dataArray.count, 0)
    71. }
    72. /// 单元测试函数名 根据命名规则命名 - 数组_预期值:添加项
    73. func test_UnitTestingBootcampViewModel_dataArray_shouldAddItems(){
    74. // Given
    75. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    76. // When
    77. let loopCount: Int = Int.random(in: 1..<100)
    78. for _ in 0 ..< loopCount{
    79. vm.addItem(item: UUID().uuidString)
    80. }
    81. // Then 断言 = 判定
    82. XCTAssertTrue(!vm.dataArray.isEmpty)
    83. XCTAssertFalse(vm.dataArray.isEmpty)
    84. XCTAssertEqual(vm.dataArray.count, loopCount)
    85. XCTAssertNotEqual(vm.dataArray.count, 0)
    86. // GreaterThan 大于
    87. XCTAssertGreaterThan(vm.dataArray.count, 0)
    88. // XCTAssertGreaterThanOrEqual
    89. // XCTAssertLessThan
    90. // XCTAssertLessThanOrEqual
    91. }
    92. /// 单元测试函数名 根据命名规则命名 - 数组_预期值:添加空白字符
    93. func test_UnitTestingBootcampViewModel_dataArray_shouldNotAddBlankString(){
    94. // Given
    95. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    96. // When
    97. vm.addItem(item: "")
    98. // Then 断言 = 判定
    99. XCTAssertTrue(vm.dataArray.isEmpty)
    100. }
    101. /// 单元测试函数名 根据命名规则命名 - 数组_预期值:添加空白字符
    102. func test_UnitTestingBootcampViewModel_dataArray_shouldNotAddBlankString2(){
    103. // Given
    104. guard let vm = viewModel else {
    105. XCTFail()
    106. return
    107. }
    108. // When
    109. vm.addItem(item: "")
    110. // Then 断言 = 判定
    111. XCTAssertTrue(vm.dataArray.isEmpty)
    112. }
    113. /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:开始为空
    114. func test_UnitTestingBootcampViewModel_selectedItem_shouldStartAsNil(){
    115. // Given
    116. // When
    117. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    118. // Then 断言 = 判定
    119. XCTAssertTrue(vm.selectedItem == nil)
    120. XCTAssertNil(vm.selectedItem)
    121. }
    122. /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:应该为空 当选择无效项
    123. func test_UnitTestingBootcampViewModel_selectedItem_shouldBeNilWhenSelectingInvalidItem(){
    124. // Given
    125. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    126. // Select valid item : 选择有效项
    127. let newItem = UUID().uuidString
    128. vm.addItem(item: newItem)
    129. vm.selectItem(item: newItem)
    130. // Select invalid item : 选择无效项
    131. // When
    132. vm.selectItem(item: UUID().uuidString)
    133. // Then 断言 = 判定
    134. XCTAssertNil(vm.selectedItem)
    135. }
    136. /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:应该选中
    137. func test_UnitTestingBootcampViewModel_selectedItem_shouldBeSelected(){
    138. // Given
    139. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    140. // When
    141. let newItem = UUID().uuidString
    142. vm.addItem(item: newItem)
    143. vm.selectItem(item: newItem)
    144. // Then 断言 = 判定
    145. XCTAssertNotNil(vm.selectedItem)
    146. XCTAssertEqual(vm.selectedItem, newItem)
    147. }
    148. /// 单元测试函数名 根据命名规则命名 - 选中项_预期值:选中_压力测试
    149. func test_UnitTestingBootcampViewModel_selectedItem_shouldBeSelected_stress(){
    150. // Given
    151. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    152. // When
    153. let loopCount: Int = Int.random(in: 1..<100)
    154. var itemsArray: [String] = []
    155. for _ in 0 ..< loopCount {
    156. let newItem = UUID().uuidString
    157. vm.addItem(item: newItem)
    158. itemsArray.append(newItem)
    159. }
    160. // 随机取一个字符串
    161. let randomItem = itemsArray.randomElement() ?? ""
    162. // 检查字符串不为空
    163. XCTAssertFalse(randomItem.isEmpty)
    164. vm.selectItem(item: randomItem)
    165. // Then 断言 = 判定
    166. XCTAssertNotNil(vm.selectedItem)
    167. XCTAssertEqual(vm.selectedItem, randomItem)
    168. }
    169. /// 单元测试函数名 根据命名规则命名 - 保存项_预期值:输出错误异常_元素没找到
    170. func test_UnitTestingBootcampViewModel_saveItem_shouldThrowError_itemNotFound(){
    171. // Given
    172. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    173. // When
    174. let loopCount: Int = Int.random(in: 1..<100)
    175. for _ in 0 ..< loopCount {
    176. vm.addItem(item: UUID().uuidString)
    177. }
    178. // Then 断言 = 判定
    179. XCTAssertThrowsError(try vm.saveItem(item: UUID().uuidString))
    180. XCTAssertThrowsError(try vm.saveItem(item: UUID().uuidString), "Should throw Item Not Found error!") { error in
    181. // 返回错误
    182. let returnedError = error as? UnitTestingBootcampViewModel.DataError
    183. // 判断错误是否相同
    184. XCTAssertEqual(returnedError, UnitTestingBootcampViewModel.DataError.itemNotFound)
    185. }
    186. }
    187. /// 单元测试函数名 根据命名规则命名 - 保存项_预期值:输出错误异常_没数据
    188. func test_UnitTestingBootcampViewModel_saveItem_shouldThrowError_noData(){
    189. // Given
    190. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    191. // When
    192. let loopCount: Int = Int.random(in: 1..<100)
    193. for _ in 0 ..< loopCount {
    194. vm.addItem(item: UUID().uuidString)
    195. }
    196. // Then 断言 = 判定
    197. do {
    198. try vm.saveItem(item: "")
    199. } catch let error {
    200. // 返回错误
    201. let returnedError = error as? UnitTestingBootcampViewModel.DataError
    202. // 判断错误是否相同
    203. XCTAssertEqual(returnedError, UnitTestingBootcampViewModel.DataError.noData)
    204. }
    205. }
    206. /// 单元测试函数名 根据命名规则命名 - 保存项_预期值:保存选项
    207. func test_UnitTestingBootcampViewModel_saveItem_shouldSaveItem(){
    208. // Given
    209. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    210. // When
    211. let loopCount: Int = Int.random(in: 1..<100)
    212. var itemsArray: [String] = []
    213. for _ in 0 ..< loopCount {
    214. let newItem = UUID().uuidString
    215. vm.addItem(item: newItem)
    216. itemsArray.append(newItem)
    217. }
    218. // 随机取一个字符串
    219. let randomItem = itemsArray.randomElement() ?? ""
    220. // 检查字符串不为空
    221. XCTAssertFalse(randomItem.isEmpty)
    222. // Then 断言 = 判定
    223. XCTAssertNoThrow(try vm.saveItem(item: randomItem))
    224. do {
    225. try vm.saveItem(item: randomItem)
    226. } catch {
    227. XCTFail()
    228. }
    229. }
    230. /// 单元测试函数名 根据命名规则命名 - 下载数据_预期值:返回选项
    231. func test_UnitTestingBootcampViewModel_downloadWithEscaping_shouldReturnItems(){
    232. // Given
    233. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    234. // When
    235. let expectation = XCTestExpectation(description: "Should return items after 3 seconds")
    236. // dropFirst: 删除第一个发布 数组值,因为初始化为空数组,取的是第二个数组,模拟服务数据返回的数组
    237. vm.$dataArray
    238. .dropFirst()
    239. .sink { returnedItems in
    240. expectation.fulfill()
    241. }
    242. .store(in: &cancellables)
    243. vm.downloadWithEscaping()
    244. // Then 断言 = 判定 GreaterThan:大于
    245. // 为了安全获取到值,设置等待 5 秒
    246. wait(for: [expectation], timeout: 5)
    247. XCTAssertGreaterThan(vm.dataArray.count, 0)
    248. }
    249. /// 单元测试函数名 根据命名规则命名 - 下载数据组合_预期值:返回选项
    250. func test_UnitTestingBootcampViewModel_downloadWithCombine_shouldReturnItems(){
    251. // Given
    252. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random())
    253. // When
    254. let expectation = XCTestExpectation(description: "Should return items after a seconds")
    255. // dropFirst: 删除第一个发布 数组值,因为初始化为空数组,取的是第二个数组,模拟服务数据返回的数组
    256. vm.$dataArray
    257. .dropFirst()
    258. .sink { returnedItems in
    259. expectation.fulfill()
    260. }
    261. .store(in: &cancellables)
    262. vm.downloadWithCombine()
    263. // Then 断言 = 判定 GreaterThan:大于
    264. // 为了安全获取到值,设置等待 5 秒
    265. wait(for: [expectation], timeout: 5)
    266. XCTAssertGreaterThan(vm.dataArray.count, 0)
    267. }
    268. /// 单元测试函数名 根据命名规则命名 - 下载数据组合_预期值:返回选项
    269. func test_UnitTestingBootcampViewModel_downloadWithCombine_shouldReturnItems2(){
    270. // Given
    271. let items: [String] = [UUID().uuidString, UUID().uuidString, UUID().uuidString, UUID().uuidString]
    272. let dataService: NewDataServiceProtocol = NewMockDataService(items: items)
    273. let vm = UnitTestingBootcampViewModel(isPremium: Bool.random(), dataService: dataService)
    274. // When
    275. let expectation = XCTestExpectation(description: "Should return items after a seconds")
    276. // dropFirst: 删除第一个发布 数组值,因为初始化为空数组,取的是第二个数组,模拟服务数据返回的数组
    277. vm.$dataArray
    278. .dropFirst()
    279. .sink { returnedItems in
    280. expectation.fulfill()
    281. }
    282. .store(in: &cancellables)
    283. vm.downloadWithCombine()
    284. // Then 断言 = 判定 GreaterThan:大于
    285. // 为了安全获取到值,设置等待 5 秒
    286. wait(for: [expectation], timeout: 5)
    287. XCTAssertGreaterThan(vm.dataArray.count, 0)
    288. XCTAssertEqual(vm.dataArray.count, items.count)
    289. }
    290. }

    3. 模拟请求数据 单元测试

      3.1 创建模拟请求数据类 NewMockDataService.swift

    1. import Foundation
    2. import SwiftUI
    3. import Combine
    4. /// 定义协议
    5. protocol NewDataServiceProtocol{
    6. func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> ())
    7. func downloadItemsWithCombine() -> AnyPublisher<[String], Error>
    8. }
    9. /// 实现模拟请求数据
    10. class NewMockDataService: NewDataServiceProtocol {
    11. let items: [String]
    12. init(items: [String]?) {
    13. self.items = items ?? [
    14. "ONE", "TWO", "THREE"
    15. ]
    16. }
    17. /// 模拟网络下载数据 escaping: 转义字符
    18. func downloadItemsWithEscaping(completion: @escaping (_ items: [String]) -> ()) {
    19. DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    20. completion(self.items)
    21. }
    22. }
    23. /// 下载组合
    24. func downloadItemsWithCombine() -> AnyPublisher<[String], Error> {
    25. // 数据转换
    26. Just(self.items)
    27. .tryMap({ publishedItems in
    28. guard !publishedItems.isEmpty else {
    29. throw URLError(.badServerResponse)
    30. }
    31. return publishedItems
    32. })
    33. .eraseToAnyPublisher()
    34. }
    35. }

      3.2 创建单元测试类 NewMockDataService_Tests.swift

    1. import XCTest
    2. import Combine
    3. /// 导入项目
    4. @testable import SwiftfulThinkingAdvancedLearning
    5. final class NewMockDataService_Tests: XCTestCase {
    6. /// 随时取消控制器
    7. var cancellable = Set<AnyCancellable>()
    8. override func setUpWithError() throws {
    9. // Put setup code here. This method is called before the invocation of each test method in the class.
    10. }
    11. override func tearDownWithError() throws {
    12. // Put teardown code here. This method is called after the invocation of each test method in the class.
    13. cancellable.removeAll()
    14. }
    15. // 单元测试函数名 根据命名规则命名 - 测试_类名_初始化_预期值:正确的设置值
    16. func test_NewMockDataService_init_doesSetValuesCorrectly() {
    17. // 执行
    18. // Given: 给定
    19. let items: [String]? = nil
    20. let items2: [String]? = []
    21. let items3: [String]? = [UUID().uuidString, UUID().uuidString]
    22. // When: 时间
    23. let dataService = NewMockDataService(items: items)
    24. let dataService2 = NewMockDataService(items: items2)
    25. let dataService3 = NewMockDataService(items: items3)
    26. // Then 然后
    27. XCTAssertFalse(dataService.items.isEmpty)
    28. XCTAssertTrue(dataService2.items.isEmpty)
    29. XCTAssertEqual(dataService3.items.count, items3?.count)
    30. }
    31. // 单元测试函数名 根据命名规则命名 - 测试_类名_下载转换数据项_预期值:正确的设置值
    32. func test_NewMockDataService_downloadItemsWithEscaping_doesReturnValues() {
    33. // 执行
    34. // Given: 给定
    35. let dataService = NewMockDataService(items: nil)
    36. // When: 时间
    37. var items: [String] = []
    38. let expectation = XCTestExpectation()
    39. dataService.downloadItemsWithEscaping { returnedItems in
    40. items = returnedItems
    41. expectation.fulfill()
    42. }
    43. // Then 然后
    44. // 等待 5 秒
    45. wait(for: [expectation], timeout: 5)
    46. // 断言两个数组大小一样
    47. XCTAssertEqual(items.count, dataService.items.count)
    48. }
    49. // 单元测试函数名 根据命名规则命名 - 测试_类名_下载数据项组合_预期值:正确的设置值
    50. func test_NewMockDataService_downloadItemsWithCombine_doesReturnValues() {
    51. // 执行
    52. // Given: 给定
    53. let dataService = NewMockDataService(items: nil)
    54. // When: 时间
    55. var items: [String] = []
    56. let expectation = XCTestExpectation()
    57. // 下载组合控制
    58. dataService.downloadItemsWithCombine()
    59. .sink { completion in
    60. switch completion{
    61. case .finished:
    62. expectation.fulfill()
    63. case .failure:
    64. XCTFail()
    65. }
    66. } receiveValue: {returnedItems in
    67. // fulfill: 完成
    68. items = returnedItems
    69. }
    70. .store(in: &cancellable)
    71. // Then 然后
    72. // 等待 5 秒
    73. wait(for: [expectation], timeout: 5)
    74. // 断言两个数组大小一样
    75. XCTAssertEqual(items.count, dataService.items.count)
    76. }
    77. // 单元测试函数名 根据命名规则命名 - 测试_类名_下载数据项组合_预期值:确实失败
    78. func test_NewMockDataService_downloadItemsWithCombine_doesFail() {
    79. // 执行
    80. // Given: 给定
    81. let dataService = NewMockDataService(items: [])
    82. // When: 时间
    83. var items: [String] = []
    84. let expectation = XCTestExpectation(description: "Does throw an error")
    85. let expectation2 = XCTestExpectation(description: "Does throw URLError.badServerResponse")
    86. // 下载组合控制
    87. dataService.downloadItemsWithCombine()
    88. .sink { completion in
    89. switch completion{
    90. case .finished:
    91. XCTFail()
    92. case .failure(let error):
    93. expectation.fulfill()
    94. //let urlError = error as? URLError
    95. // 断言,判定
    96. //XCTAssertEqual(urlError, URLError(.badServerResponse))
    97. // 错误判断
    98. if error as? URLError == URLError(.badServerResponse) {
    99. expectation2.fulfill()
    100. }
    101. }
    102. } receiveValue: {returnedItems in
    103. // fulfill: 完成
    104. items = returnedItems
    105. }
    106. .store(in: &cancellable)
    107. // Then 然后
    108. // 等待 5 秒
    109. wait(for: [expectation, expectation2], timeout: 5)
    110. // 断言两个数组大小一样
    111. XCTAssertEqual(items.count, dataService.items.count)
    112. }
    113. }

    4. 创建单元测试 View,调用测试的 ViewModel UnitTestingBootcampView.swift

    1. import SwiftUI
    2. /*
    3. 1. Unit Test : 单元测试
    4. - test the business logic in your app : 测试应用中的业务逻辑
    5. 2. UI Test : 界面测试
    6. - test the UI of your app : 测试应用中的界面
    7. */
    8. /// 单元测试
    9. struct UnitTestingBootcampView: View {
    10. @StateObject private var vm: UnitTestingBootcampViewModel
    11. init(isPremium: Bool){
    12. _vm = StateObject(wrappedValue: UnitTestingBootcampViewModel(isPremium: isPremium))
    13. }
    14. var body: some View {
    15. Text(vm.isPremium.description)
    16. }
    17. }
    18. struct UnitTestingBootcampView_Previews: PreviewProvider {
    19. static var previews: some View {
    20. UnitTestingBootcampView(isPremium: true)
    21. }
    22. }
  • 相关阅读:
    Android 界面灰化处理(特殊节日)
    flink-sql所有表连接器
    软考高项-论文写作
    菊风视频能力平台通过浙江省信创适配实验室验证测试
    02-Nginx安装与常用命令
    华钜同创:跨境电商运营如何优化Listing文案
    全能代码生成器,自动生成前后端代码、生成项目框架、生成JavaBean、生成数据库文档、自动化部署项目(TableGo v8.0.0)
    【PyQt学习篇 · ③】:QObject - 神奇的对象管理工具
    LeetCode 341(Python)-扁平化嵌套列表迭代器
    万界星空科技商业开源MES,技术支持+项目合作
  • 原文地址:https://blog.csdn.net/u011193452/article/details/133879306