当前位置:   article > 正文

UnitTesting 单元测试

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. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/598410
推荐阅读
相关标签
  

闽ICP备14008679号