分享我的第一次java Selenium自動(dòng)化測(cè)試框架開(kāi)發(fā)過(guò)程
由于公司的開(kāi)發(fā)團(tuán)隊(duì)偏向于使用Java技術(shù),而且公司倡導(dǎo)學(xué)習(xí)開(kāi)源技術(shù),所以我選擇用Java語(yǔ)言來(lái)進(jìn)行Selenium WebDriver的自動(dòng)化框架開(kāi)發(fā)。由于本人沒(méi)有Java開(kāi)發(fā)經(jīng)驗(yàn),以前雖然學(xué)過(guò)QTP但從沒(méi)有接觸過(guò)Selenium,正好通過(guò)這個(gè)機(jī)會(huì)能學(xué)習(xí)一下自動(dòng)化測(cè)試,同時(shí)也學(xué)習(xí)一下基本的Java開(kāi)發(fā)過(guò)程。
一、首先是搭建框架開(kāi)發(fā)環(huán)境
按照網(wǎng)上的方法部署eclipse,建立TestAction工程,并Import引用JDK和Selenium-2.44完整包
二、繼續(xù)引用和安裝相關(guān)jar包
1、首先是要滿足數(shù)據(jù)驅(qū)動(dòng)(場(chǎng)景用例和動(dòng)作用例、數(shù)據(jù)用例都需要放到excel表上),就需要引用jxl.rar包(實(shí)現(xiàn)調(diào)用和操作excel);
2、需要實(shí)現(xiàn)自動(dòng)化框架(有測(cè)試套件、測(cè)試層)就需要通過(guò)eclipse安裝TestNg(網(wǎng)上有相關(guān)教程);
三、構(gòu)建框架的樣例代碼
1、實(shí)現(xiàn)能夠?qū)xcel用例數(shù)據(jù)的調(diào)用(通過(guò)jxl的引用),創(chuàng)建ExcelData.java類文件(專門(mén)用于對(duì)excel的調(diào)用),以下截取部分代碼樣例:
/** * @param fileName excel文件名 * @param caseName sheet名 */ public ExcelData(String fileName, String caseName) { super(); this.fileName = fileName; this.caseName = caseName; } /** * 獲得excel表中的數(shù)據(jù) */ public Object[][] getExcelData() throws BiffException, IOException { workbook = Workbook.getWorkbook(new File(getPath())); sheet = workbook.getSheet(caseName); rows = sheet.getRows(); columns = sheet.getColumns(); // 為了返回值是Object[][],定義一個(gè)多行單列的二維數(shù)組 @SuppressWarnings("unchecked") HashMap<String, String>[][] arrmap = new HashMap[rows - 1][1]; // 對(duì)數(shù)組中所有元素hashmap進(jìn)行初始化 if (rows > 1) { for (int i = 0; i < rows - 1; i++) { arrmap[i][0] = new HashMap<String, String>(); } } else { System.out.println("excel中沒(méi)有數(shù)據(jù)"); } // 獲得首行的列名,作為hashmap的key值 for (int c = 0; c < columns; c++) { String cellvalue = sheet.getCell(c, 0).getContents(); arrkey.add(cellvalue); } // 遍歷所有的單元格的值添加到hashmap中 for (int r = 1; r < rows; r++) { for (int c = 0; c < columns; c++) { String cellvalue = sheet.getCell(c, r).getContents(); arrmap[r - 1][0].put(arrkey.get(c), cellvalue); } } return arrmap; } /** * 獲得excel文件的路徑 * @return * @throws IOException */ public String getPath() throws IOException { File directory = new File("."); sourceFile = directory.getCanonicalPath() + "\\src\\source\\" + fileName + ".xls"; return sourceFile; }
2、實(shí)現(xiàn)對(duì)瀏覽器的調(diào)用,考慮到兼容性,需要同時(shí)滿足對(duì)Chrome、FireFox、IE三大瀏覽器的調(diào)用,我們需要準(zhǔn)備相關(guān)驅(qū)動(dòng)chromedriver.exe、IEDriverServer.exe,這兩驅(qū)動(dòng)都是谷歌和IE官方提供的,可以從網(wǎng)上下載到;而FireFox不需要下載驅(qū)動(dòng),只要安裝瀏覽器就可調(diào)用(Selenium和FireFox屬于一個(gè)團(tuán)隊(duì)開(kāi)發(fā)出來(lái)的,待遇就是不一樣)。
有了瀏覽器驅(qū)動(dòng)后(我們把驅(qū)動(dòng)放到工程目錄的WebDriver文件夾下,方便按相對(duì)路徑統(tǒng)一調(diào)用),我們就需要一個(gè)能調(diào)用瀏覽器的類,以下提供核心代碼樣例:
public static WebDriver getChromeDriver(String url) { //加載Google驅(qū)動(dòng) //System.setProperty("webdriver.chrome.driver","D:\\java\\chromedriver.exe"); System.setProperty("webdriver.chrome.driver",System.getProperties().getProperty("user.dir")+"\\WebDriver\\chromedriver.exe"); ChromeOptions options = new ChromeOptions(); //通過(guò)配置參數(shù)禁止data;的出現(xiàn) options.addArguments("--user-data-dir="+System.getProperties().getProperty("user.home")+"/AppData/Local/Google/Chrome/User Data/Default"); //通過(guò)配置參數(shù)刪除“您使用的是不受支持的命令行標(biāo)記:--ignore-certificate-errors。穩(wěn)定性和安全性會(huì)有所下降?!碧崾? options.addArguments("--start-maximized","allow-running-insecure-content", "--test-type"); WebDriver driver = new ChromeDriver(options); driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); driver.navigate().to(url); return driver; } public static WebDriver getFireFoxDriver(String url){ System.setProperty("webdriver.firefox.bin", "D:\\Program Files\\Mozilla Firefox\\firefox.exe"); // TODO Auto-generated method stub WebDriver driver = new FirefoxDriver(); //Puts a Implicit wait, Will wait for 10 seconds before throwing exception driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); //Launch website driver.navigate().to(url); return driver; } public static WebDriver getIEDriver(String url){ //System.setProperty("webdriver.ie.driver", "D:\\java\\IE64\\IEDriverServer.exe"); System.setProperty("webdriver.ie.driver", System.getProperties().getProperty("user.dir")+"\\WebDriver\\IE32\\IEDriverServer.exe"); DesiredCapabilities capabilities = DesiredCapabilities.internetExplorer(); capabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS,true); capabilities.setPlatform(Platform.WINDOWS); capabilities.setCapability("silent", true); // TODO Auto-generated method stub WebDriver driver = new InternetExplorerDriver(capabilities); //Puts a Implicit wait, Will wait for 10 seconds before throwing exception driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); //Launch website driver.navigate().to(url); return driver; }
3、寫(xiě)一個(gè)以數(shù)據(jù)驅(qū)動(dòng)的場(chǎng)景類,來(lái)進(jìn)行單個(gè)事務(wù)的用例跑測(cè)
(1)首行我們需要用TesgNg提供的數(shù)據(jù)驅(qū)動(dòng)方法(@DataProvider),來(lái)獲取一個(gè)場(chǎng)景的用例表數(shù)據(jù),這個(gè)場(chǎng)景從excel的第一個(gè)附表獲取
通過(guò)action名,調(diào)取用例表(用例表是以action名命名的附表),用例表如下所示(ExpectedObject表示用例校驗(yàn)對(duì)象的頁(yè)面Element標(biāo)簽,用;分隔,分號(hào)前面的表示ID,分號(hào)后面的表示xpath):
以下為用例表數(shù)據(jù)獲取的代碼:
@DataProvider(name="action") public Object[][] Numbers() throws BiffException, IOException{ getActionString = actionData.getActionStr(1);//獲取第一個(gè)場(chǎng)景的broswer、url、action名 ExcelData e=new ExcelData("testdata", getActionString.get(2)); return e.getExcelData(); }
然后通過(guò)Java的反射機(jī)制,實(shí)現(xiàn)動(dòng)態(tài)的獲取具體事務(wù)類和執(zhí)行相關(guān)操作(每個(gè)事務(wù)的類名和方法名都與action場(chǎng)景名一致),以下截選相關(guān)場(chǎng)景的部分調(diào)用代碼:
@Test(dataProvider="action") public void testAction(HashMap<String, String> data) throws BiffException, IOException { try { Class<?> MyClass = Class.forName(packageName+"."+getActionString.get(2)); Method method = MyClass.getMethod(getActionString.get(2),WebDriver.class); @SuppressWarnings("unused") String [] results = (String []) method.invoke(null,driver); String ExpObject=data.get("ExpectedObject"); String ExpObject_by=ExpObject.split(";")[0].toString(); String ExpObject_Desc=ExpObject.split(";")[1].toString(); if(ExpObject_by.length()>0){ Assert.assertEquals(driver.findElement(By.id(ExpObject_by)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"驗(yàn)證結(jié)果:"); } else if(ExpObject_Desc.length()>0){ Assert.assertEquals(driver.findElement(By.xpath(ExpObject_Desc)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"驗(yàn)證結(jié)果:"); } WebDriverDemo.WebSleep(500); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }
另外說(shuō)明的是,調(diào)用瀏覽器的方法,需要明確是放在@BeforeMethod中,還是在@BeforeClass中,如果是登錄校驗(yàn)測(cè)試,就要保證每次執(zhí)行測(cè)試方法都要打開(kāi)一次瀏覽器和關(guān)閉一次瀏覽器,那么我們就要把調(diào)用瀏覽器,和關(guān)閉瀏覽器的方法放到@BeforeMethod中和@AfterMethod中。其他業(yè)務(wù)測(cè)試,只要在一個(gè)套件類中打開(kāi)一次瀏覽器和關(guān)閉一次瀏覽器就可以,所以用到的是@BeforeClass和@AfterClass。
4、我們需要再寫(xiě)一個(gè)以動(dòng)作(關(guān)鍵詞)驅(qū)動(dòng)的場(chǎng)景類
同樣,調(diào)用第二個(gè)場(chǎng)景的用例表,樣例代碼如下:
@DataProvider(name="action") public Object[][] Numbers() throws BiffException, IOException{ getActionString = actionData.getActionStr(2);//獲取第二個(gè)場(chǎng)景的broswer、url、action名 ExcelData e=new ExcelData("testdata", getActionString.get(2)); return e.getExcelData(); }
然后在測(cè)試方法中,動(dòng)態(tài)的調(diào)用具體操作動(dòng)作,獲取WebElement標(biāo)簽的方法,包括通過(guò)By ID或者By xpath,操作動(dòng)作以最常見(jiàn)的兩個(gè)為例(sendKeys、click),以下為樣例代碼節(jié)選:
@Test(dataProvider="action") public void testAction(HashMap<String, String> data) throws BiffException, IOException { //driver.manage().timeouts().implicitlyWait(5,TimeUnit.SECONDS);//找不到element就再給5秒查找 try { WebElement TestWebElement = null; String SetObject=data.get("SetObject").trim(); String SetObject_by=SetObject.split(";")[0].toString(); String SetObject_Desc=SetObject.split(";")[1].toString(); if(SetObject_by.length()>0){ TestWebElement=driver.findElement(By.id(SetObject_by)); } else if(SetObject_Desc.length()>0){ TestWebElement=driver.findElement(By.xpath(SetObject_Desc)); } if(data.get("SetOperate").equals("sendKeys")){ TestWebElement.clear(); TestWebElement.sendKeys(data.get("SetValue")); }else if(data.get("SetOperate").equals("click")){ TestWebElement.click(); } String ExpObject=data.get("ExpectedObject").trim(); if(ExpObject.length()>0){ String ExpObject_by=ExpObject.split(";")[0].toString(); String ExpObject_Desc=ExpObject.split(";")[1].toString(); if(ExpObject_by.length()>0){ Assert.assertEquals(driver.findElement(By.id(ExpObject_by)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"驗(yàn)證結(jié)果:"); } else if(ExpObject_Desc.length()>0){ Assert.assertEquals(driver.findElement(By.xpath(ExpObject_Desc)).getText(),data.get("ExpectedData"), getActionString.get(2)+data.get("ID")+"驗(yàn)證結(jié)果:"); } } WebDriverDemo.WebSleep(500); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }
這段方法所調(diào)用的用例表如下所示(以登錄為例):
5、剩下就是業(yè)務(wù)擴(kuò)展類了,所有復(fù)雜的事務(wù)都可以單獨(dú)建立測(cè)試類和方法(方便擴(kuò)展維護(hù),只需要在excel場(chǎng)景表中定義后就能調(diào)用,利用的是Java反射機(jī)制),在這里就不舉例了。
四、實(shí)現(xiàn)測(cè)試套件調(diào)用和報(bào)告輸出
有了以上步驟,一個(gè)可擴(kuò)展的自動(dòng)化框架已經(jīng)基本形成,但是還達(dá)不到大規(guī)模應(yīng)用測(cè)試和腳本方便可移植,這時(shí)候我們引入Ant(可以在Eclipse中安裝插件,可以直接上網(wǎng)下載后引用),為了能輸出漂亮一點(diǎn)的報(bào)告格式,我們還引入一個(gè)saxon-8.7.jar。
有了Ant后,我們就可以建議build.xml文件,就能一鍵bulid我們以上的自動(dòng)化代碼,并將執(zhí)行測(cè)試后的結(jié)果輸出成報(bào)告。
1、首先我們需要編輯好測(cè)試套件調(diào)用的testng.xml,簡(jiǎn)單舉例如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="Suite" parallel="false"> <test verbose="2" name="Test_Action"> <!--<groups> <run> <include name="aaa" /> <include name="bbb" /> <include name="ccc" /> </run> </groups>--> <classes> <class name="TestBrowser.ExcActions"/> <class name="TestBrowser.ExcActions2"/> </classes> </test> <!-- Default test --> </suite> <!-- Default suite -->
2、然后我們需要編輯好一個(gè)能引用基礎(chǔ)jar包、build測(cè)試代碼、調(diào)用testng、輸出漂亮報(bào)告的build.xml文件
<?xml version="1.0" encoding="UTF-8"?> <project name= "TestAction" basedir= "." default="testoutput"><!--default設(shè)置為run表示只執(zhí)行腳本,設(shè)為testoutput表示執(zhí)行完腳本并輸出視圖報(bào)告--> <echo message="import libs" /> <property name= "lib.dir" value= "lib" /> <!--<property name="libdir" location="${basedir}/lib" />--> <!--<property name="testng.output.dir" location="${basedir}/test-output" />--> <path id= "test.classpath" > <!-- adding the saxon jar to your classpath --> <fileset dir= "${lib.dir}" includes= "*.jar" /> <fileset dir="${basedir}/selenium-2.44.0"> <include name="selenium-java-2.44.0.jar" /> <include name="libs/*.jar" /> </fileset> </path> <taskdef name="testng" classname="org.testng.TestNGAntTask" classpathref="test.classpath" rel="external nofollow" /> <target name="clean"> <delete dir="build"/> </target> <target name="compile" depends="clean"> <echo message="mkdir"/> <mkdir dir="build/classes"/> <javac srcdir="src" destdir="build/classes" debug="on" encoding="UTF-8" includeAntRuntime="false"> <classpath refid="test.classpath"/> </javac> </target> <path id="runpath"> <path refid="test.classpath"/> <pathelement location="build/classes"/> </path> <target name="run" depends="compile"> <testng classpathref="runpath" rel="external nofollow" outputDir="test-output"> <xmlfileset dir="${basedir}" includes="testng.xml"/> <jvmarg value="-ea" /> </testng> </target> <target name= "testoutput" depends="run"> <xslt in= "test-output/testng-results.xml" style= "test-output/testng-results.xsl" out= "test-output/index1.html" > <!-- you need to specify the directory here again --> <param name= "testNgXslt.outputDir" expression= "${basedir}/test-output/" /> <param name="testNgXslt.showRuntimeTotals" expression="true" /> <classpath refid= "test.classpath" /> </xslt> </target> </project>
3、完成這些后,我們就可以通過(guò)Eclipse直接Run As Ant Build我們的自動(dòng)化腳本了,輸出一份還算漂亮的報(bào)告:
同時(shí),需要在事務(wù)操作類中,對(duì)實(shí)際結(jié)果和預(yù)期結(jié)果進(jìn)行比較,并將測(cè)試結(jié)果寫(xiě)入excel的用例表中,如下:
String[] result=new String [2]; result[0] = driver.findElement(By.xpath(pars.get(3).split(";")[1].toString())).getText(); result[1] = pars.get(4); if(result[0].equals(result[1])){//pars.size()-1 ActionsDemo.modifyExcel(Thread.currentThread().getStackTrace()[1].getMethodName(),k,5,"通過(guò)"); } else { ActionsDemo.modifyExcel(Thread.currentThread().getStackTrace()[1].getMethodName(),k,5,"失敗"); }
五、實(shí)現(xiàn)自動(dòng)化框架腳本的遷移調(diào)用
以上的腳本始終是在Eclipse下編譯和調(diào)用的,如果要實(shí)現(xiàn)靈活遷移,隨便換任何一臺(tái)只裝了JDK的電腦都能運(yùn)行,那么我們就要來(lái)點(diǎn)改造
1、首行是保證我們寫(xiě)的代碼中,所以需要引用文件的地方,都用相對(duì)路徑的方式,避免代碼包遷移后需要改路徑。
2、通過(guò)批處理調(diào)用build文件及用例文件,調(diào)用時(shí)也是通過(guò)批處理自動(dòng)找到相關(guān)路徑,避免用絕對(duì)路徑。
3、需要用環(huán)境變量的地方,盡量用批處理的方式實(shí)現(xiàn),甚至最好是不用配置環(huán)境變量,直接調(diào)用相引用相對(duì)命令文件的路徑調(diào)用
以下舉個(gè)通過(guò)bat批處理調(diào)用Ant來(lái)執(zhí)行整個(gè)框架代碼的build:
@echo off ::先將測(cè)試用例文件拷到用戶目錄下 copy src\source\testdata.xls %UserProfile%\src\source %cd%\org.apache.ant_1.9.6\bin\ant.bat -buildfile build.xml echo 在%cd%\test-output下查看測(cè)試報(bào)告 pause
六、進(jìn)一步實(shí)現(xiàn)自動(dòng)化的持續(xù)集成
在以上基礎(chǔ)上,我們還可以通過(guò)jenkins實(shí)現(xiàn)對(duì)自動(dòng)化腳本的調(diào)用,以及達(dá)到每日構(gòu)建,持續(xù)集成開(kāi)發(fā)的要求。
1、首先部署jenkins(網(wǎng)上有相關(guān)方法),由于本人公司一直在用jenkins,我就省了搭建部署這一步,直接將以上的自動(dòng)化框架腳本上傳
2、自動(dòng)化腳本完整目錄(包括代碼、用例、lib、引用的jar、build.xml文件等)上傳到SVN(再自動(dòng)從SVN下到j(luò)enkins所在服務(wù)器)
3、在jenkins中新建一個(gè)測(cè)試項(xiàng)目TestAction,主要配置如下:
4、配置完后,就可以立即構(gòu)建(如果碰到相關(guān)報(bào)錯(cuò)問(wèn)題,就按輸出的提示進(jìn)行處理),構(gòu)建成功后,就可以在HTML_Report中看到測(cè)試結(jié)果:
七、后續(xù)處理
到此為止,一個(gè)完整的Selenium自動(dòng)化框架就出來(lái)了,要說(shuō)好用不,不好說(shuō),還得經(jīng)過(guò)實(shí)踐的檢驗(yàn),但是以上這個(gè)思考過(guò)程和框架的演進(jìn)過(guò)程,應(yīng)該也是值得借鑒的,畢竟這是我這幾天摸索和學(xué)習(xí)的過(guò)程,對(duì)于一個(gè)沒(méi)有從事過(guò)自動(dòng)化測(cè)試,而且沒(méi)有做過(guò)Java開(kāi)發(fā)的測(cè)試人員來(lái)說(shuō),這只是個(gè)開(kāi)始。
目前來(lái)看,這個(gè)框架在架構(gòu)分層上,還是不夠清晰,有很多要改進(jìn)的東西,從技術(shù)上來(lái)說(shuō),我已經(jīng)實(shí)現(xiàn)了我的目標(biāo)(學(xué)習(xí)自動(dòng)化測(cè)試),但是在整體架構(gòu)和代碼重構(gòu)上,還有很多工作沒(méi)做,以下貼出一份Selenium自動(dòng)化框架的分層結(jié)構(gòu),以便后期按照這個(gè)標(biāo)準(zhǔn)進(jìn)行改進(jìn):
測(cè)試數(shù)據(jù)層:獨(dú)立封裝數(shù)據(jù);
頁(yè)面對(duì)象層:封裝頁(yè)面對(duì)象,共頁(yè)面任務(wù)層做調(diào)用;
頁(yè)面任務(wù)層:實(shí)現(xiàn)各個(gè)獨(dú)立頁(yè)面的操作;
測(cè)試層:實(shí)現(xiàn)頁(yè)面測(cè)試;
測(cè)試套件層:實(shí)現(xiàn)測(cè)試層的管理調(diào)用;
到此這篇關(guān)于分享我的第一次java Selenium自動(dòng)化測(cè)試框架開(kāi)發(fā)過(guò)程的文章就介紹到這了,更多相關(guān)java Selenium自動(dòng)化測(cè)試 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解SpringBoot緩存的實(shí)例代碼(EhCache 2.x 篇)
這篇文章主要介紹了詳解SpringBoot緩存的實(shí)例代碼(EhCache 2.x 篇),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07myeclipse開(kāi)發(fā)servlet_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
MyEclipse,是在eclipse基礎(chǔ)上加上自己的插件開(kāi)發(fā)而成的功能強(qiáng)大的企業(yè)級(jí)集成開(kāi)發(fā)環(huán)境,主要用于Java、Java EE以及移動(dòng)應(yīng)用的開(kāi)發(fā)。下面這篇文章主要給大家介紹了關(guān)于myeclipse開(kāi)發(fā)servlet的相關(guān)資料,需要的朋友可以參考下。2017-07-07Java實(shí)現(xiàn)簡(jiǎn)單訂餐系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡(jiǎn)單訂餐系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01聊聊SpringBoot使用Nacos進(jìn)行服務(wù)注冊(cè)發(fā)現(xiàn)與配置管理問(wèn)題
Nacos支持基于DNS和基于RPC的服務(wù)發(fā)現(xiàn)(可以作為springcloud的注冊(cè)中心)、動(dòng)態(tài)配置服務(wù)(可以做配置中心)、動(dòng)態(tài)?DNS?服務(wù)。本文重點(diǎn)給大家介紹SpringBoot使用Nacos進(jìn)行服務(wù)注冊(cè)發(fā)現(xiàn)與配置管理,感興趣的朋友一起看看吧2022-01-01java異步寫(xiě)日志到文件中實(shí)現(xiàn)代碼
這篇文章主要介紹了java異步寫(xiě)日志到文件中實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-04-04springboot整合SSE技術(shù)開(kāi)發(fā)小結(jié)
本文主要介紹了springboot整合SSE技術(shù)開(kāi)發(fā)小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11