1. 程式人生 > >.NET單元測試的藝術-3.測試程式碼

.NET單元測試的藝術-3.測試程式碼

開篇:上一篇我們學習單元測試和核心技術:存根、模擬物件和隔離框架,它們是我們進行高質量單元測試的技術基礎。本篇會集中在管理和組織單元測試的技術,以及如何確保在真實專案中進行高質量的單元測試。

系列目錄:

1.入門

2.核心技術

3.測試程式碼

一、測試層次和組織

1.1 測試專案的兩種目錄結構

  (1)整合測試和單元測試在同一個專案裡,但放在不同的目錄和名稱空間裡。基礎類放在單獨的資料夾裡。

  (2)整合測試和單元測試位於不同的專案中,有不同的名稱空間。

實踐中推薦使用第二種目錄結構,因為如果我們不把這兩種測試分開,人們可能就不會經常地執行這些測試。既然測試都寫好了,為什麼人們不願意按照需要執行它們呢?一個原因是:開發人員有可能懶得執行測試,或者沒有實踐執行測試。

1.2 構建綠色安全區

  將整合測試和單元測試分開放置,其實就給團隊的開發人員構建了綠色安全區,這個區只包含單元測試。

  因為整合測試的本質決定了它執行時間較長,開發人員很有可能每天執行多次單元測試,較少執行整合測試。

單元測試全部通過至少可以使開發人員對程式碼質量比較有信心,專注於提高編碼效率。而且我們應該將測試自動化,編寫每日構建指令碼,並藉助持續整合工具幫助我們自動執行這些指令碼。

1.3 將測試類對映到被測試程式碼

  (1)將測試對映到專案

  建立一個測試專案,用被測試專案的名字加上字尾.UnitTests來命名。

  例如:Manulife.MyLibrary → Manulife.MyLibrary.UnitTests 和 Manulife.MyLibrary.IntegrationTests,這種方法看起來簡單直觀,開發人員能夠從專案名稱找到對應的所有測試。

  (2)將測試對映到類

  ① 每個被測試類或者被測試工作單元對應一個測試類:LogAnalyzer → LogAnalyzer.UnitTests

  ② 每個功能對應一個測試類:有一個LoginManager類,測試方法為ChangePassword(這個方法測試用例特別多,需要單獨放在一個測試類裡邊) → 建立兩個類 LoginManagerTests 和 LoginManagerTests-ChangePassword,前者只包含對ChangePassword方法的測試,後者包含該類其他所有測試。

  (3)將測試對映到具體的工作單元入口

  測試方法的命名應該有意義,這樣人們可以很容易地找到所有相關的測試方法。

  這裡,迴歸一下第一篇中提到的測試方法名稱的規範,一般包含三個部分:[UnitOfWorkName]_[ScenarioUnderTest]_[ExpectedBehavior]

    • UnitOfWorkName  被測試的方法、一組方法或者一組類
    • Scenario  測試進行的假設條件,例如“登入失敗”,“無效使用者”或“密碼正確”等
    • ExpectedBehavior  在測試場景指定的條件下,你對被測試方法行為的預期  

  示例:IsValidFileName_BadExtension_ReturnsFalse,IsValidFileName_EmptyName_Throws 等

1.4 注入橫切關注點

  當需要處理類似時間管理、異常或日誌的橫切關注點時,使用它們的地方會非常多,如果把它們實現成可注入的,產生的程式碼會很容易測試,但卻很難閱讀和理解。這裡我們來看一個例子,假設應用程式使用當前時間進行寫日誌,相關程式碼如下:

    public static class TimeLogger
    {
        public static string CreateMessage(string info)
        {
            return DateTime.Now.ToShortDateString() + " " + info;
        }
    }

  為了使這段程式碼容易測試,如果使用之前的依賴注入技術,那麼我們需要建立一個ITimeProvider介面,還必須在每個用到DateTime的地方使用到這個介面。這樣做非常耗時,實際上,還有更直接的方法解決這個問題。

  Step1.建立一個名為SystemTime的定製類,在所有的產品程式碼裡邊使用這個定製類,而非標準的內建類DateTime。

    public class SystemTime
    {
        private static DateTime _date;

        public static void Set(DateTime custom)
        {
            _date = custom;
        }

        public static void Reset()
        {
            _date = DateTime.MinValue;
        }

        public static DateTime Now
        {
            get
            {
                // 如果設定了時間,SystemTime就返回假時間,否則返回真時間
                if (_date != DateTime.MinValue)
                {
                    return _date;
                }
                return DateTime.Now;
            }
        }
    }
View Code

  閱讀這段程式碼,其中有一個小技巧:SystemTime類提供一個特殊方法Set,它會修改系統中的當前時間,也就是說,每個使用這個SystemTime類的人看到的都是你指定的日期和時間。有了這樣的程式碼,每個使用這個SystemTime類的人看到的都會是你指定的日期和時間。

  Step2.在測試專案中使用SystemTime進行測試。

    [TestFixture]
    public class TimeLoggerTests
    {
        [Test]
        public void SettingSystemTime_Always_ChangesTime()
        {
            SystemTime.Set(new DateTime(2000, 1, 1));
            string output = TimeLogger.CreateMessage("a");

            StringAssert.Contains("2000/1/1", output);
        }

        /// <summary>
        /// 在每個測試結束時重置日期
        /// </summary>
        [TearDown]
        public void AfterEachTest()
        {
            SystemTime.Reset();
        }
    }
View Code

  在測試中,我們首先假定設定一個日期,然後進行斷言。並且藉助TearDown方法,確保當前測試不會改變其他測試的值

Note : 這樣做的好處就在於不用注入一大堆介面,我們所付出的代價僅僅在於在測試類中加入一個簡單的[TearDown]方法,確保當前測試不會改變其他測試的值。

1.5 使用繼承使測試程式碼可重用

  推薦大家在測試程式碼中使用繼承機制,通過實現基類,可以較好地展現面向物件的魔力。在實踐中,一般有三種模式會被使用到:

  (1)抽象測試基礎結構類模式

    /// <summary>
    /// 測試類整合模式
    /// </summary>
    [TestFixture]
    public class BaseTestsClass
    {
        /// <summary>
        /// 重構為通用可讀的工具方法,由派生類使用
        /// </summary>
        /// <returns>FakeLogger</returns>
        public ILogger FakeTheLogger()
        {
            LoggingFacility.Logger = Substitute.For<ILogger>();
            return LoggingFacility.Logger;
        }

        [TearDown]
        public void ClearLogger()
        {
            // 測試之間要重置靜態資源
            LoggingFacility.Logger = null;
        }
    }

    [TestFixture]
    public class LogAnalyzerTests : BaseTestsClass
    {
        [Test]
        public void Analyze_EmptyFile_ThrowsException()
        {
            // 呼叫基類的輔助方法
            FakeTheLogger();

            LogAnalyzer analyzer = new LogAnalyzer();
            analyzer.Analyze("myemptyfile.txt");

            // 測試方法的其餘部分
        }
    }
View Code

  使用此模式要注意繼承最好不要超過一層,如果繼承層數過多,不僅可讀性急劇下降,編譯也很容易出錯。

  (2)測試類類模板模式

    /// <summary>
    /// 測試模板類模式
    /// </summary>
    [TestFixture]
    public abstract class TemplateStringParserTests
    {
        [Test]
        public abstract void TestGetStringVersionFromHeader_SingleDigit_Found();
        [Test]
        public abstract void TestGetStringVersionFromHeader_WithMinorVersion_Found();
        [Test]
        public abstract void TestGetStringVersionFromHeader_WithRevision_Found();
    }

    [TestFixture]
    public class XMLStrignParserTests : TemplateStringParserTests
    {
        protected IStringParser GetParser(string input)
        {
            return new XMLStringParser(input);
        }

        [Test]
        public override void TestGetStringVersionFromHeader_SingleDigit_Found()
        {
            IStringParser parser = GetParser("<Header>1</Header>");

            string versionFromHeader = parser.GetTextVersionFromHeader();
            Assert.AreEqual("1", versionFromHeader);
        }

        [Test]
        public override void TestGetStringVersionFromHeader_WithMinorVersion_Found()
        {
            IStringParser parser = GetParser("<Header>1.1</Header>");

            string versionFromHeader = parser.GetTextVersionFromHeader();
            Assert.AreEqual("1.1", versionFromHeader);
        }

        [Test]
        public override void TestGetStringVersionFromHeader_WithRevision_Found()
        {
            IStringParser parser = GetParser("<Header>1.1.1</Header>");

            string versionFromHeader = parser.GetTextVersionFromHeader();
            Assert.AreEqual("1.1", versionFromHeader);
        }
    }
View Code

  使用此模式可以確保開發者不會遺忘重要的測試,基類包含了抽象的測試方法,派生類必須實現這些抽象方法。

  (3)抽象測試驅動類模式

    /// <summary>
    /// 抽象“填空”測試驅動類模式
    /// </summary>
    public abstract class FillInTheBlankStringParserTests
    {
        // 返回介面的抽象方法
        protected abstract IStringParser GetParser(string input);
        // 抽象輸入方法(屬性),為派生類提供特定格式的資料
        protected abstract string HeaderVersion_SingleDigit { get; }
        protected abstract string HeaderVersion_WithMinorVersion { get; }
        protected abstract string HeaderVersion_WithRevision { get; }
        // 如果需要,預先為派生類定義預期的輸出
        public const string EXPECTED_SINGLE_DIGIT = "1";
        public const string EXPECTED_WITH_MINORVERSION = "1.1";
        public const string EXPECTED_WITH_REVISION = "1.1.1";

        [Test]
        public void TestGetStringVersionFromHeader_SingleDigit_Found()
        {
            string input = HeaderVersion_SingleDigit;
            IStringParser parser = GetParser(input);

            string versionFromHeader = parser.GetTextVersionFromHeader();
            Assert.AreEqual(EXPECTED_SINGLE_DIGIT, versionFromHeader);
        }

        [Test]
        public void TestGetStringVersionFromHeader_WithMinorVersion_Found()
        {
            string input = HeaderVersion_WithMinorVersion;
            IStringParser parser = GetParser(input);

            string versionFromHeader = parser.GetTextVersionFromHeader();
            Assert.AreEqual(EXPECTED_WITH_MINORVERSION, versionFromHeader);
        }

        [Test]
        public void TestGetStringVersionFromHeader_WithRevision_Found()
        {
            string input = HeaderVersion_WithRevision;
            IStringParser parser = GetParser(input);

            string versionFromHeader = parser.GetTextVersionFromHeader();
            Assert.AreEqual(EXPECTED_WITH_REVISION, versionFromHeader);
        }
    }

    public class DBLogStringParserTests : GenericParserTests<DBLogStringParser>
    {
        protected override string GetInputHeaderSingleDigit()
        {
            return "Header;1";
        }

        protected override string GetInputHeaderWithMinorVersion()
        {
            return "Header;1.1";
        }

        protected override string GetInputHeaderWithRevision()
        {
            return "Header;1.1.1";
        }
    }
View Code

  此模式在基類中實現測試方法,並提供派生類可以實現的抽象方法鉤子。當然,只是大部分的測試程式碼在基類中,派生類也可以加入自己的特殊測試。

  此模式的要點在於:你不是具體地測試一個類,而是測試產品程式碼中的一個介面或者基類。

  當然,在.NET中我們也可以通過泛型來實現此模式,例如下面的程式碼:

    public abstract class GenericParserTests<T> where T : IStringParser // 01.定義引數的泛型約束
    {
        protected abstract string GetInputHeaderSingleDigit();
        protected abstract string GetInputHeaderWithMinorVersion();
        protected abstract string GetInputHeaderWithRevision();

        // 02.返回泛型變數而非介面
        protected T GetParser(string input)
        {
            // 03.返回泛型
            return (T)Activator.CreateInstance(typeof(T), input);
        }

        [Test]
        public void TestGetStringVersionFromHeader_SingleDigit_Found()
        {
            string input = GetInputHeaderSingleDigit();
            T parser = GetParser(input);

            bool result = parser.HasCorrectHeader();
            Assert.AreEqual(false, result);
        }

        [Test]
        public void TestGetStringVersionFromHeader_WithMinorVersion_Found()
        {
            string input = GetInputHeaderWithMinorVersion();
            T parser = GetParser(input);

            bool result = parser.HasCorrectHeader();
            Assert.AreEqual(false, result);
        }

        [Test]
        public void TestGetStringVersionFromHeader_WithRevision_Found()
        {
            string input = GetInputHeaderWithRevision();
            T parser = GetParser(input);

            bool result = parser.HasCorrectHeader();
            Assert.AreEqual(false, result);
        }
    }

    public class DBLogStringParserTests : GenericParserTests<DBLogStringParser>
    {
        protected override string GetInputHeaderSingleDigit()
        {
            return "Header;1";
        }

        protected override string GetInputHeaderWithMinorVersion()
        {
            return "Header;1.1";
        }

        protected override string GetInputHeaderWithRevision()
        {
            return "Header;1.1.1";
        }
    }
View Code

二、優秀單元測試的支柱

  要編寫優秀的單元測試,它們應該同時具有 可靠性可維護性可讀性

2.1 編寫可靠的測試

  一個可靠的測試能讓你覺得自己對事態瞭如指掌,能夠從容應對。以下是一些指導原則和技術:

  (1)決定何時刪除或修改測試

  一旦測試寫好並通過,通常我們不應該修改或刪除這些測試,因為它們是我們得綠色保護網。但是,有時候我們還是需要修改或者刪除測試,所以需要理解什麼情況下修改或刪除測試會帶來問題,什麼情況下又是合理的。一般來說,如果有產品缺陷、測試缺陷、語義或者API更改或者是由於衝突或無效測試,我們需要修改和刪除測試程式碼。

  (2)避免測試中的邏輯

  隨著測試中邏輯的增多,出現測試缺陷的機率就會呈現指數倍的增長。如果單元測試中包含了下列語句就是包含了不應該有的邏輯:

  • switch、if或else語句;
  • foreach、for或while迴圈;

  這種做法不值得推薦,因為這樣的測試可讀性較差,也比較脆弱。通常來說,一個單元測試應該是一系列方法的呼叫和斷言,但是不包含控制流程語句,甚至不應該將斷言語句放在try-catch中

  (3)只測試一個關注點

  如果我們的單元測試對多個物件進行了斷言,那麼這個測試有可能測試了多個關注點。在一個單元測試中驗證多個關注點會使得事情變得複雜,卻沒有什麼價值。你應該在分開的、獨立的單元測試中驗證多餘的關注點,這樣才能發現真正失敗的地方。

  (4)把單元測試和整合測試分開

  掐面討論了測試的綠色安全區,我們需要的就是準備一個單獨的單元測試專案,專案中僅包含那些在記憶體中執行,結果穩定,可重複執行的測試。

  (5)用程式碼審查確保程式碼覆蓋率

  如果覆蓋率低於20%,說明我們缺少很多測試,我們不會知道下一個開發人員將怎麼修改我們得程式碼。如果沒有回失敗的測試,可能就不會發現這些錯誤。

2.2 編寫可維護性的測試

  可維護性是大多數開發者在編寫單元測試時面對的核心問題之一。為此我們需要:

  (1)只測試公共契約

  (2)刪除重複測試(去除重複程式碼)

  (3)實施測試隔離

  測試隔離的基本概念是:一個測試應該總是在它自己的小世界中執行,與其他類似或不同的工作的測試隔離,甚至不知道其他測試的存在

2.3 編寫可讀性的測試

  不可讀的測試幾乎沒有任何意義,它是我們向專案的下一代開發者講述的故事,幫助開發者理解一個應用程式的組成及其開端。

  (1)單元測試命名

  這個前面我們討論過,應該包括三部分:被測試方法名_測試場景_預期行為,如果開發人員都是用這種規範,其他的開發人員就能很容易進入專案,理解測試。

  (2)變數命名

  通過合理命名變數,你可以確保閱讀測試的人可以儘快地理解你要驗證什麼(相對於理解產品程式碼中你想要實現什麼)。請看下面的一個例子:

    [Test]
    public void BadlyNameTest()
    {
        LogAnalyzer log = new LogAnalyzer();
        int result = log.GetLineCount("abc.txt");

        Assert.AreEqual(-100, result);
    }

    [Test]
    public void GoodNameTest()
    {
        LogAnalyzer log = new LogAnalyzer();
        int result = log.GetLineCount("abc.txt");
        const int COULD_NOT_READ_FILE = -100;

        Assert.AreEqual(-COULD_NOT_READ_FILE, result);
    }
View Code

  經過改進後,我們會很容易理解這個返回值的意義。

  (3)有意義的斷言

  只有當測試確實需要,並且找不到別的辦法使測試更清晰時,你才應該編寫定製的斷言資訊。編寫好的斷言資訊就像編寫好的異常資訊,一不小心就會犯錯,使讀者產生誤解,浪費他們的時間。

  (4)斷言和操作分離

  為了可讀性,請不要把斷言和方法呼叫寫在同一行。

    // 斷言和操作寫在了同一行
    Assert.AreEqual(-COULD_NOT_READ_FILE, log.GetLineCount("abc.txt"));

三、小結

  這一篇我們學習了:

  • 儘量將測試自動化,儘可能多次地執行測試,儘可能持續地進行產品交付;
  • 把整合測試和單元測試分開,為整個團隊構建一個綠色安全區,該區域中所有的測試都必須通過;
  • 按照專案和型別組織測試,把測試分別放在不同的目錄、資料夾或者名稱空間中;
  • 使用測試類層次,對一個層次中相關的幾個類進行同一組測試,或者對共享一個通用介面或者基類的型別進行同一組測試;
  • 優秀單元測試具有三大支柱:可讀性、可維護性與可靠性,它們相輔相成。
  • 如果人們能讀懂你的測試,就能理解和維護測試,如果測試能夠通過,它們也會信任測試。一旦實現這個目標,你就能知道系統是否正常工作,具有了處理變更和在需要時修改程式碼的能力;

附件下載

  本系列文章的示例程式碼:點此下載

參考資料

      The Art of Unit Testing

  (1)Roy Osherove 著,金迎 譯,《單元測試的藝術(第2版)》

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

.NET單元測試藝術-3.測試程式碼

開篇:上一篇我們學習單元測試和核心技術:存根、模擬物件和隔離框架,它們是我們進行高質量單元測試的技術基礎。本篇會集中在管理和組織單元測試的技術,以及如何確保在真實專案中進行高質量的單元測試。 系列目錄: 1.入門 2.核心技術 3.測試程式碼 一、測試層次和組織 1.1 測試專案的兩種目錄

.NET單元測試藝術-2.核心技術

開篇:上一篇我們學習基本的單元測試基礎知識和入門例項。但是,如果我們要測試的方法依賴於一個外部資源,如檔案系統、資料庫、Web服務或者其他難以控制的東西,那又該如何編寫測試呢?為了解決這些問題,我們需要建立測試存根、偽物件及模擬物件。這一篇中我們會開始接觸這些核心技術,藉助存根破除依賴,使用模擬物件進行互動測

.NET單元測試藝術-1.入門

開篇:最近在看Roy Osherove的《單元測試的藝術》一書,頗有收穫。因此,將其記錄下來,並分為四個部分分享成文,與各位Share。本篇作為入門,介紹了單元測試的基礎知識,例如:如何使用一個測試框架,基本的自動化測試屬性等等,還有對應的三種測試型別。相信你可以對編寫單元測試從一無所知到及格水平,這也是原書

.Net單元測試業務實踐

elm com cursor sco toa void 重點 aci data code[class*="langua

python unittest單元測試框架-3用例執行順序、多級目錄、用例類別、

1.用例執行順序 unittest預設會按照ascii碼的順序,依次執行。類名——方法名排序,使用discover也是預設排序。如果不想使用預設排序,就使用testsuite測試集的方式。 import unittest class TestB(unittest.TestCase): def se

【C#】【xUnit】【Moq】.NET單元測試Mock框架Moq初探!

在TDD開發模型中,經常是在編碼的同時進行單元測試的編寫,由於現代軟體開發不可能是一個人完成的工作,所以在定義好介面的時候我們就可以進行自己功能的開發(介面不能經常變更),而我們呼叫他人的功能時只需要使用介面即可。 但我們在編寫自己的單元測試並進行功能驗證的時候,如果介面的實現人還沒有完成程式碼怎麼

Visual Studio及TFS進行單元測試、負載測試程式碼覆蓋率、每日構建配置

這是以前在VS2010上讓同事做的相關工作的備案,現在VS2012都出來了,2010都快過期,帖出來共享一下。 一、單元測試 微軟官方給出的指導和示例在這裡。一步一步按照說明下來就可以完成。(不截圖說明了)。此外部落格園有一篇文章說得比較清楚,在這裡。 二、建立並執行包含單元測試的負載測試 微軟官方給出的指

.net 單元測試工具:自帶 mstest

轉載地址: http://blog.csdn.net/metal1/article/details/9630451    在我的專案中使用的VS自帶測試工具MSTEST而不是NUNIT,   有時需要自動化測試VS中的程式碼,便於整合,需要在命令列執行。  

JustMock .NET單元測試利器(二)JustMock基礎

Mock是Telerik®JustMock框架中的主要類。Mock用於建立例項和靜態模擬,安排和驗證行為。 本文將介紹 “Mock”的基本用法: 首先我們建立一個IFoo物件 publicinter

JustMock .NET單元測試利器(一)

1.什麼是Mock? Mock一詞是指模仿或者效仿,用於建立例項和靜態模擬、安排和驗證行為。在軟體開發中提及"mock",通常理解為模擬物件。模擬物件的概念就是我們想要建立一個可以替代實際物件的物件,

.Net單元測試之NMock

下面是一個使用NMock的快速教程: 示例 現在我們來做一個簡單的“Hello”例子,測試 Hello 類的 Greet() 方法,Hello 類依賴於一個 Person 物件,並將根據 Person 的名字向對應的賬號傳送祝賀(Greet)。這個例子很簡單,其實用不到模擬物件,不過用來理解 NMock 是很

C#.NEt-GDI+中的Pen測試

ace reat thread 技術 img 效果圖 log pub dsm using System; using System.Collections.Generic; using System.ComponentModel; using System.Data;

單元測試、集成測試、系統測試和驗收測試的聯系和區別

是否 功能 條件 黑盒測試 模塊 期望值 設計 tex 代碼 根據不同的測試階段,測試可以分為單元測試、集成測試、系統測試和驗收測試體現了測試由小到大、又內至外、循序漸進的測試過程和分而治之的思想。 單元測試的粒度最小,一般由開發小組采用白盒方式來測試,主要測試單元是

.net異步性能測試(包括ASP.NET MVC WebAPI異步方法)

睡眠時間 問題 none seconds await 數字 val http httpget 很久沒有寫博客了,今年做的產品公司這兩天剛剛開了發布會,稍微清閑下來,想想我們做的產品還有沒有性能優化空間,於是想到了.Net的異步可以優化性能,但到底能夠提升多大的比例呢?恰好有

黑盒測試、白盒測試單元測試、集成測試、系統測試、驗收測試的區別與聯系

角色 同時 驗收 center 調試 需求 lan 說明書 錯誤 黑盒測試、白盒測試、單元測試、集成測試、系統測試、驗收測試的區別與聯系   接下來為大家細心講述一下各種測試應用的環境及作用。 一、測試環境和角色 黑盒測試、白盒測試、單元測試、集成測試、系統測試、

10-3測試

中心 更新 最大值 貪心 數據 擴展歐幾裏得 處理 需要 擴展 還行吧,T2想到正解但是有點困沒打優化,應該是能A的,T3的話不會樹上倍增啊要多多鞏固。 題意: T1:數論題,求在%a意義下最接近b的數,且多少次可以達到這個數 T2:定義一個刺激的矩陣是每行中最小值的

接口自動化測試Python(3)_使用python對Json進行操作

封裝 family name com src 技術分享 init with elf 如何使用Python對excel進行簡單的操作,這個對接口自動化測試很重要 一. 準備一份Json文檔,如下格式: { "register":{"mobile":"15500000090"}

ASP.NET WebAPI使用Swagger生成測試文檔

domain lease resources 警告 term model trim tno star ASP.NET WebAPI使用Swagger生成測試文檔 SwaggerUI是一個簡單的Restful API測試和文檔工具。簡單、漂亮、易用(官方demo)。通過讀取J

python編輯基礎與http接口測試_9.3章節

stat 成功 dig 基礎 客戶 指定 isdigit sin 單元 HTTP狀態碼分類查詢方法+單元測試 1 #StatusCodeType.py 2 class StatusCodeType(): 3 def getStatusType(self,St

monkey測試入門3

more 支持 device 安裝完成 art 修改 ins 們的 TP 本文要感謝一起戰鬥過的點時小夥伴,程童鞋 打開開始 輸入cmd 看到它的目錄地址 然後把adb壓縮包解壓到該地址 插入數據線 打開手機設置打開開發者選項 打開USB調試 右鍵點擊我的電腦 選擇管理